summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/sync/test_bookmark_deduping.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/tests/sync/test_bookmark_deduping.js')
-rw-r--r--toolkit/components/places/tests/sync/test_bookmark_deduping.js1290
1 files changed, 1290 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/sync/test_bookmark_deduping.js b/toolkit/components/places/tests/sync/test_bookmark_deduping.js
new file mode 100644
index 0000000000..0c6c79496a
--- /dev/null
+++ b/toolkit/components/places/tests/sync/test_bookmark_deduping.js
@@ -0,0 +1,1290 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_duping_local_newer() {
+ let mergeTelemetryCounts;
+ let buf = await openMirror("duping_local_newer", {
+ recordStepTelemetry(name, took, counts) {
+ if (name == "merge") {
+ mergeTelemetryCounts = counts.filter(({ count }) => count > 0);
+ }
+ },
+ });
+ let localModified = new Date();
+
+ info("Start with empty local and mirror with merged items");
+ await storeRecords(
+ buf,
+ [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAA5"],
+ dateAdded: localModified.getTime(),
+ },
+ {
+ id: "bookmarkAAA5",
+ parentid: "menu",
+ type: "bookmark",
+ bmkUri: "http://example.com/a",
+ title: "A",
+ dateAdded: localModified.getTime(),
+ },
+ ],
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Add newer local dupes");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "bookmarkAAA1",
+ title: "A",
+ url: "http://example.com/a",
+ dateAdded: localModified,
+ lastModified: localModified,
+ },
+ {
+ guid: "bookmarkAAA2",
+ title: "A",
+ url: "http://example.com/a",
+ dateAdded: localModified,
+ lastModified: localModified,
+ },
+ {
+ guid: "bookmarkAAA3",
+ title: "A",
+ url: "http://example.com/a",
+ dateAdded: localModified,
+ lastModified: localModified,
+ },
+ ],
+ });
+
+ info("Add older remote dupes");
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAAA", "bookmarkAAA4", "bookmarkAAA5"],
+ modified: localModified / 1000 - 5,
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "menu",
+ type: "bookmark",
+ bmkUri: "http://example.com/a",
+ title: "A",
+ keyword: "kw",
+ tags: ["remote", "tags"],
+ modified: localModified / 1000 - 5,
+ },
+ {
+ id: "bookmarkAAA4",
+ parentid: "menu",
+ type: "bookmark",
+ bmkUri: "http://example.com/a",
+ title: "A",
+ modified: localModified / 1000 - 5,
+ },
+ ]);
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply({
+ remoteTimeSeconds: localModified / 1000,
+ });
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["bookmarkAAA4", "bookmarkAAAA", PlacesUtils.bookmarks.menuGuid],
+ "Should leave A4, A, menu with new remote structure unmerged"
+ );
+ deepEqual(
+ mergeTelemetryCounts,
+ [
+ { name: "items", count: 9 },
+ { name: "dupes", count: 2 },
+ ],
+ "Should record telemetry with dupe counts"
+ );
+
+ let menuInfo = await PlacesUtils.bookmarks.fetch(
+ PlacesUtils.bookmarks.menuGuid
+ );
+ deepEqual(
+ changesToUpload,
+ {
+ menu: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: menuInfo.dateAdded.getTime(),
+ title: menuInfo.title,
+ children: [
+ "bookmarkAAAA",
+ "bookmarkAAA4",
+ "bookmarkAAA3",
+ "bookmarkAAA5",
+ ],
+ },
+ },
+ // Note that we always reupload the deduped local item, because content
+ // matching doesn't account for attributes like keywords, synced annos, or
+ // tags.
+ bookmarkAAAA: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkAAAA",
+ type: "bookmark",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: menuInfo.title,
+ dateAdded: localModified.getTime(),
+ title: "A",
+ bmkUri: "http://example.com/a",
+ },
+ },
+ // Unchanged from local.
+ bookmarkAAA4: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkAAA4",
+ type: "bookmark",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: menuInfo.title,
+ dateAdded: localModified.getTime(),
+ title: "A",
+ bmkUri: "http://example.com/a",
+ },
+ },
+ bookmarkAAA3: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkAAA3",
+ type: "bookmark",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: menuInfo.title,
+ dateAdded: localModified.getTime(),
+ title: "A",
+ bmkUri: "http://example.com/a",
+ },
+ },
+ bookmarkAAA5: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkAAA5",
+ type: "bookmark",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: menuInfo.title,
+ dateAdded: localModified.getTime(),
+ title: "A",
+ bmkUri: "http://example.com/a",
+ },
+ },
+ },
+ "Should uploaded newer deduped local items"
+ );
+
+ 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: "bookmarkAAA4",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "A",
+ url: "http://example.com/a",
+ },
+ {
+ guid: "bookmarkAAA3",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "A",
+ url: "http://example.com/a",
+ },
+ {
+ guid: "bookmarkAAA5",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 3,
+ title: "A",
+ url: "http://example.com/a",
+ },
+ ],
+ },
+ "Should dedupe local multiple bookmarks with similar contents"
+ );
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_duping_remote_newer() {
+ let buf = await openMirror("duping_remote_new");
+ let localModified = new Date();
+
+ info("Set up mirror");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ // Shouldn't dupe to `folderA11111` because its sync status is "NORMAL".
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "A",
+ children: [
+ {
+ // Shouldn't dupe to `bookmarkG111`.
+ guid: "bookmarkGGGG",
+ url: "http://example.com/g",
+ title: "G",
+ },
+ ],
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderAAAAAA"],
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "menu",
+ type: "folder",
+ title: "A",
+ children: ["bookmarkGGGG"],
+ },
+ {
+ id: "bookmarkGGGG",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "G",
+ bmkUri: "http://example.com/g",
+ },
+ ]),
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Insert local dupes");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ // Should dupe to `folderB11111`.
+ guid: "folderBBBBBB",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "B",
+ dateAdded: localModified,
+ lastModified: localModified,
+ children: [
+ {
+ // Should dupe to `bookmarkC222`.
+ guid: "bookmarkC111",
+ url: "http://example.com/c",
+ title: "C",
+ dateAdded: localModified,
+ lastModified: localModified,
+ },
+ {
+ // Should dupe to `separatorF11` because the positions are the same.
+ guid: "separatorFFF",
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ dateAdded: localModified,
+ lastModified: localModified,
+ },
+ ],
+ },
+ {
+ // Shouldn't dupe to `separatorE11`, because the positions are different.
+ guid: "separatorEEE",
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ dateAdded: localModified,
+ lastModified: localModified,
+ },
+ {
+ // Shouldn't dupe to `bookmarkC222` because the parents are different.
+ guid: "bookmarkCCCC",
+ url: "http://example.com/c",
+ title: "C",
+ dateAdded: localModified,
+ lastModified: localModified,
+ },
+ {
+ // Should dupe to `queryD111111`.
+ guid: "queryDDDDDDD",
+ url: "place:maxResults=10&sort=8",
+ title: "Most Visited",
+ dateAdded: localModified,
+ lastModified: localModified,
+ },
+ ],
+ });
+
+ // Make sure we still dedupe this even though it doesn't have SYNC_STATUS.NEW
+ PlacesTestUtils.setBookmarkSyncFields({
+ guid: "folderBBBBBB",
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.UNKNOWN,
+ });
+
+ // Not a candidate for `bookmarkH111` because we didn't dupe `folderAAAAAA`.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: "folderAAAAAA",
+ guid: "bookmarkHHHH",
+ url: "http://example.com/h",
+ title: "H",
+ dateAdded: localModified,
+ lastModified: localModified,
+ });
+
+ info("Make remote changes");
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: [
+ "folderAAAAAA",
+ "folderB11111",
+ "folderA11111",
+ "separatorE11",
+ "queryD111111",
+ ],
+ dateAdded: localModified.getTime(),
+ modified: localModified / 1000 + 5,
+ },
+ {
+ id: "folderB11111",
+ parentid: "menu",
+ type: "folder",
+ title: "B",
+ children: ["bookmarkC222", "separatorF11"],
+ dateAdded: localModified.getTime(),
+ modified: localModified / 1000 + 5,
+ },
+ {
+ id: "bookmarkC222",
+ parentid: "folderB11111",
+ type: "bookmark",
+ bmkUri: "http://example.com/c",
+ title: "C",
+ dateAdded: localModified.getTime(),
+ modified: localModified / 1000 + 5,
+ },
+ {
+ id: "separatorF11",
+ parentid: "folderB11111",
+ type: "separator",
+ dateAdded: localModified.getTime(),
+ modified: localModified / 1000 + 5,
+ },
+ {
+ id: "folderA11111",
+ parentid: "menu",
+ type: "folder",
+ title: "A",
+ children: ["bookmarkG111"],
+ dateAdded: localModified.getTime(),
+ modified: localModified / 1000 + 5,
+ },
+ {
+ id: "bookmarkG111",
+ parentid: "folderA11111",
+ type: "bookmark",
+ bmkUri: "http://example.com/g",
+ title: "G",
+ dateAdded: localModified.getTime(),
+ modified: localModified / 1000 + 5,
+ },
+ {
+ id: "separatorE11",
+ parentid: "menu",
+ type: "separator",
+ dateAdded: localModified.getTime(),
+ modified: localModified / 1000 + 5,
+ },
+ {
+ id: "queryD111111",
+ parentid: "menu",
+ type: "query",
+ bmkUri: "place:maxResults=10&sort=8",
+ title: "Most Visited",
+ dateAdded: localModified.getTime(),
+ modified: localModified / 1000 + 5,
+ },
+ ])
+ );
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply({
+ remoteTimeSeconds: localModified / 1000,
+ });
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [PlacesUtils.bookmarks.menuGuid],
+ "Should leave menu with new remote structure unmerged"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: [
+ "bookmarkCCCC",
+ "bookmarkHHHH",
+ "folderAAAAAA",
+ "menu",
+ "separatorEEE",
+ ],
+ deleted: [],
+ },
+ "Should not upload deduped 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: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "A",
+ children: [
+ {
+ guid: "bookmarkGGGG",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "G",
+ url: "http://example.com/g",
+ },
+ {
+ guid: "bookmarkHHHH",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "H",
+ url: "http://example.com/h",
+ },
+ ],
+ },
+ {
+ guid: "folderB11111",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: "B",
+ children: [
+ {
+ guid: "bookmarkC222",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "C",
+ url: "http://example.com/c",
+ },
+ {
+ guid: "separatorF11",
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ index: 1,
+ title: "",
+ },
+ ],
+ },
+ {
+ guid: "folderA11111",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 2,
+ title: "A",
+ children: [
+ {
+ guid: "bookmarkG111",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "G",
+ url: "http://example.com/g",
+ },
+ ],
+ },
+ {
+ guid: "separatorE11",
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ index: 3,
+ title: "",
+ },
+ {
+ guid: "queryD111111",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 4,
+ title: "Most Visited",
+ url: "place:maxResults=10&sort=8",
+ },
+ {
+ guid: "separatorEEE",
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ index: 5,
+ title: "",
+ },
+ {
+ guid: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 6,
+ title: "C",
+ url: "http://example.com/c",
+ },
+ ],
+ },
+ {
+ 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 dedupe matching NEW bookmarks"
+ );
+
+ ok(
+ (
+ await PlacesTestUtils.fetchBookmarkSyncFields(
+ "menu________",
+ "folderB11111",
+ "bookmarkC222",
+ "separatorF11",
+ "folderA11111",
+ "bookmarkG111",
+ "separatorE11",
+ "queryD111111"
+ )
+ ).every(info => info.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NORMAL)
+ );
+
+ 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_duping_both() {
+ let buf = await openMirror("duping_both");
+ let now = Date.now();
+
+ info("Start with empty mirror");
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Add local dupes");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ // `folderAAAAA1` is older than `folderAAAAAA`, but we should still flag
+ // it for upload because it has a new structure (`bookmarkCCCC`).
+ guid: "folderAAAAA1",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "A",
+ dateAdded: new Date(now - 10000),
+ lastModified: new Date(now - 5000),
+ children: [
+ {
+ // Shouldn't upload, since `bookmarkBBBB` is newer.
+ guid: "bookmarkBBB1",
+ title: "B",
+ url: "http://example.com/b",
+ dateAdded: new Date(now - 10000),
+ lastModified: new Date(now - 5000),
+ },
+ {
+ // Should upload, since `bookmarkCCCC` doesn't exist on the server and
+ // has no content matches.
+ guid: "bookmarkCCCC",
+ title: "C",
+ url: "http://example.com/c",
+ dateAdded: new Date(now - 10000),
+ lastModified: new Date(now - 5000),
+ },
+ ],
+ },
+ {
+ // `folderDDDDD1` should keep complete local structure, but we'll still
+ // flag it for reupload because it's newer than `folderDDDDDD`.
+ guid: "folderDDDDD1",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "D",
+ dateAdded: new Date(now - 10000),
+ lastModified: new Date(now + 5000),
+ children: [
+ {
+ guid: "bookmarkEEE1",
+ title: "E",
+ url: "http://example.com/e",
+ dateAdded: new Date(now - 10000),
+ lastModified: new Date(now - 5000),
+ },
+ ],
+ },
+ {
+ // `folderFFFFF1` should keep complete remote value and structure, so
+ // we shouldn't upload it or its children.
+ guid: "folderFFFFF1",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "F",
+ dateAdded: new Date(now - 10000),
+ lastModified: new Date(now - 5000),
+ children: [
+ {
+ guid: "bookmarkGGG1",
+ title: "G",
+ url: "http://example.com/g",
+ dateAdded: new Date(now - 10000),
+ lastModified: new Date(now - 5000),
+ },
+ ],
+ },
+ ],
+ });
+
+ info("Add remote dupes");
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderAAAAAA", "folderDDDDDD", "folderFFFFFF"],
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "menu",
+ type: "folder",
+ title: "A",
+ dateAdded: now - 10000,
+ modified: now / 1000 + 5,
+ children: ["bookmarkBBBB"],
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ bmkUri: "http://example.com/b",
+ title: "B",
+ dateAdded: now - 10000,
+ modified: now / 1000 + 5,
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "menu",
+ type: "folder",
+ title: "D",
+ dateAdded: now - 10000,
+ modified: now / 1000 - 5,
+ children: ["bookmarkEEEE"],
+ },
+ {
+ id: "bookmarkEEEE",
+ parentid: "folderDDDDDD",
+ type: "bookmark",
+ bmkUri: "http://example.com/e",
+ title: "E",
+ dateAdded: now - 10000,
+ modified: now / 1000 + 5,
+ },
+ {
+ id: "folderFFFFFF",
+ parentid: "menu",
+ type: "folder",
+ title: "F",
+ dateAdded: now - 10000,
+ modified: now / 1000 + 5,
+ children: ["bookmarkGGGG", "bookmarkHHHH"],
+ },
+ {
+ id: "bookmarkGGGG",
+ parentid: "folderFFFFFF",
+ type: "bookmark",
+ bmkUri: "http://example.com/g",
+ title: "G",
+ dateAdded: now - 10000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "bookmarkHHHH",
+ parentid: "folderFFFFFF",
+ type: "bookmark",
+ bmkUri: "http://example.com/h",
+ title: "H",
+ dateAdded: now - 10000,
+ modified: now / 1000 + 5,
+ },
+ ]);
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply({
+ remoteTimeSeconds: now / 1000,
+ });
+
+ let menuInfo = await PlacesUtils.bookmarks.fetch(
+ PlacesUtils.bookmarks.menuGuid
+ );
+ deepEqual(
+ changesToUpload,
+ {
+ menu: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: menuInfo.dateAdded.getTime(),
+ title: menuInfo.title,
+ children: ["folderAAAAAA", "folderDDDDDD", "folderFFFFFF"],
+ },
+ },
+ folderAAAAAA: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "folderAAAAAA",
+ type: "folder",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: menuInfo.title,
+ dateAdded: now - 10000,
+ title: "A",
+ children: ["bookmarkBBBB", "bookmarkCCCC"],
+ },
+ },
+ bookmarkCCCC: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkCCCC",
+ type: "bookmark",
+ parentid: "folderAAAAAA",
+ hasDupe: true,
+ parentName: "A",
+ dateAdded: now - 10000,
+ title: "C",
+ bmkUri: "http://example.com/c",
+ },
+ },
+ folderDDDDDD: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "folderDDDDDD",
+ type: "folder",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: menuInfo.title,
+ dateAdded: now - 10000,
+ title: "D",
+ children: ["bookmarkEEEE"],
+ },
+ },
+ },
+ "Should upload new and newer locally deduped items"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ 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: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "C",
+ url: "http://example.com/c",
+ },
+ ],
+ },
+ {
+ guid: "folderDDDDDD",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: "D",
+ children: [
+ {
+ guid: "bookmarkEEEE",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "E",
+ url: "http://example.com/e",
+ },
+ ],
+ },
+ {
+ guid: "folderFFFFFF",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 2,
+ title: "F",
+ children: [
+ {
+ guid: "bookmarkGGGG",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "G",
+ url: "http://example.com/g",
+ },
+ {
+ guid: "bookmarkHHHH",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "H",
+ url: "http://example.com/h",
+ },
+ ],
+ },
+ ],
+ },
+ "Should change local GUIDs for mixed older and newer items"
+ );
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_applying_two_empty_folders_doesnt_smush() {
+ let buf = await openMirror("applying_two_empty_folders_doesnt_smush");
+
+ info("Set up empty mirror");
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make remote changes");
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "mobile",
+ parentid: "places",
+ type: "folder",
+ children: ["emptyempty01", "emptyempty02"],
+ },
+ {
+ id: "emptyempty01",
+ parentid: "mobile",
+ type: "folder",
+ title: "Empty",
+ },
+ {
+ id: "emptyempty02",
+ parentid: "mobile",
+ type: "folder",
+ title: "Empty",
+ },
+ ])
+ );
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply();
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: [],
+ deleted: [],
+ },
+ "Should not upload records for remote-only value changes"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.mobileGuid,
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: "mobile",
+ children: [
+ {
+ guid: "emptyempty01",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "Empty",
+ },
+ {
+ guid: "emptyempty02",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: "Empty",
+ },
+ ],
+ },
+ "Should not smush 1 and 2"
+ );
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_applying_two_empty_folders_matches_only_one() {
+ let buf = await openMirror("applying_two_empty_folders_doesnt_smush");
+
+ info("Set up empty mirror");
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make local changes");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ children: [
+ {
+ guid: "emptyempty02",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "Empty",
+ },
+ {
+ guid: "emptyemptyL0",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "Empty",
+ },
+ ],
+ });
+
+ info("Make remote changes");
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "mobile",
+ parentid: "places",
+ type: "folder",
+ children: ["emptyempty01", "emptyempty02", "emptyempty03"],
+ },
+ {
+ id: "emptyempty01",
+ parentid: "mobile",
+ type: "folder",
+ title: "Empty",
+ },
+ {
+ id: "emptyempty02",
+ parentid: "mobile",
+ type: "folder",
+ title: "Empty",
+ },
+ {
+ id: "emptyempty03",
+ parentid: "mobile",
+ type: "folder",
+ title: "Empty",
+ },
+ ])
+ );
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [PlacesUtils.bookmarks.mobileGuid],
+ "Should leave mobile with new remote structure unmerged"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["mobile"],
+ deleted: [],
+ },
+ "Should not upload records after applying empty folders"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.mobileGuid,
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: "mobile",
+ children: [
+ {
+ guid: "emptyempty01",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "Empty",
+ },
+ {
+ guid: "emptyempty02",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: "Empty",
+ },
+ {
+ guid: "emptyempty03",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 2,
+ title: "Empty",
+ },
+ ],
+ },
+ "Should apply 1 and dedupe L0 to 3"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+// Bug 747699.
+add_task(async function test_duping_mobile_bookmarks() {
+ let buf = await openMirror("duping_mobile_bookmarks");
+
+ info("Set up empty mirror with localized mobile root title");
+ let mobileInfo = await PlacesUtils.bookmarks.fetch(
+ PlacesUtils.bookmarks.mobileGuid
+ );
+ await PlacesUtils.bookmarks.update({
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ title: "Favoritos do celular",
+ });
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make local changes");
+ await PlacesUtils.bookmarks.insert({
+ guid: "bookmarkAAA1",
+ parentGuid: PlacesUtils.bookmarks.mobileGuid,
+ title: "A",
+ url: "http://example.com/a",
+ });
+
+ info("Make remote changes");
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "mobile",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAAA"],
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "mobile",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ },
+ ])
+ );
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [PlacesUtils.bookmarks.mobileGuid],
+ "Should leave mobile with new remote structure unmerged"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["mobile"],
+ deleted: [],
+ },
+ "Should not upload records after applying deduped mobile bookmark"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.mobileGuid,
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: "Favoritos do celular",
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "A",
+ url: "http://example.com/a",
+ },
+ ],
+ },
+ "Should dedupe A1 to A with different parent title"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ // Restore the original mobile root title.
+ await PlacesUtils.bookmarks.update({
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ title: mobileInfo.title,
+ });
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_duping_invalid() {
+ // To check if invalid items are prevented from deduping
+
+ info("Set up empty mirror");
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "bookmarkAAA1",
+ title: "A",
+ url: "http://example.com/a",
+ },
+ ],
+ });
+
+ let buf = await openMirror("duping_invalid");
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAA2"],
+ },
+ {
+ id: "bookmarkAAA2",
+ parentid: "menu",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ },
+ ]);
+
+ // Invalidate bookmarkAAA2 so that it does not dedupe to bookmarkAAA1
+ await buf.db.execute(
+ `UPDATE items SET
+ validity = :validity
+ WHERE guid = :guid`,
+ {
+ validity: Ci.mozISyncedBookmarksMerger.VALIDITY_REPLACE,
+ guid: "bookmarkAAA2",
+ }
+ );
+
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ changesToUpload.menu.cleartext.children,
+ ["bookmarkAAA1"],
+ "Should upload A1 in menu"
+ );
+ ok(
+ !changesToUpload.bookmarkAAA1.tombstone,
+ "Should not upload tombstone for A1"
+ );
+ ok(changesToUpload.bookmarkAAA2.tombstone, "Should upload tombstone for A2");
+ await assertLocalTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "bookmarkAAA1",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "A",
+ url: "http://example.com/a",
+ },
+ ],
+ },
+ "No deduping of invalid items"
+ );
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});