/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ add_task(async function test_value_combo() { let buf = await openMirror("value_combo"); let now = Date.now(); info("Set up mirror with existing bookmark to update"); await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.menuGuid, children: [ { guid: "mozBmk______", url: "https://mozilla.org", title: "Mozilla", tags: ["moz", "dot", "org"], }, ], }); await storeRecords( buf, shuffle([ { id: "menu", parentid: "places", type: "folder", children: ["mozBmk______"], }, { id: "mozBmk______", parentid: "menu", type: "bookmark", title: "Mozilla", bmkUri: "https://mozilla.org", tags: ["moz", "dot", "org"], }, ]), { needsMerge: false } ); await PlacesTestUtils.markBookmarksAsSynced(); info("Insert new local bookmark to upload"); let [bzBmk] = await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.toolbarGuid, children: [ { guid: "bzBmk_______", url: "https://bugzilla.mozilla.org", title: "Bugzilla", tags: ["new", "tag"], }, ], }); info("Insert remote bookmarks and folder to apply"); await storeRecords( buf, shuffle([ { id: "mozBmk______", parentid: "menu", type: "bookmark", title: "Mozilla home page", bmkUri: "https://mozilla.org", tags: ["browsers"], }, { id: "toolbar", parentid: "places", type: "folder", children: ["fxBmk_______", "tFolder_____"], }, { id: "fxBmk_______", parentid: "toolbar", type: "bookmark", title: "Get Firefox", bmkUri: "http://getfirefox.com", tags: ["taggy", "browsers"], dateAdded: now, }, { id: "tFolder_____", parentid: "toolbar", type: "folder", title: "Mail", children: ["tbBmk_______"], dateAdded: now, }, { id: "tbBmk_______", parentid: "tFolder_____", type: "bookmark", title: "Get Thunderbird", bmkUri: "http://getthunderbird.com", keyword: "tb", dateAdded: now, }, ]) ); info("Apply remote"); let observer = expectBookmarkChangeNotifications({ skipTags: true, ignoreDates: false, }); let localTimeSeconds = Math.floor(now / 1000); let changesToUpload = await buf.apply({ localTimeSeconds, notifyInStableOrder: true, }); deepEqual( await buf.fetchUnmergedGuids(), [PlacesUtils.bookmarks.toolbarGuid], "Should leave toolbar with new remote structure unmerged" ); let menuInfo = await PlacesUtils.bookmarks.fetch( PlacesUtils.bookmarks.menuGuid ); deepEqual( changesToUpload, { bzBmk_______: { tombstone: false, counter: 3, synced: false, cleartext: { id: "bzBmk_______", type: "bookmark", parentid: "toolbar", hasDupe: true, parentName: BookmarksToolbarTitle, dateAdded: bzBmk.dateAdded.getTime(), bmkUri: "https://bugzilla.mozilla.org/", title: "Bugzilla", tags: ["new", "tag"], }, }, toolbar: { tombstone: false, counter: 1, synced: false, cleartext: { id: "toolbar", type: "folder", parentid: "places", hasDupe: true, parentName: "", dateAdded: menuInfo.dateAdded.getTime(), title: BookmarksToolbarTitle, children: ["fxBmk_______", "tFolder_____", "bzBmk_______"], }, }, }, "Should upload new local bookmarks and parents" ); let localItemIds = await PlacesUtils.promiseManyItemIds([ "fxBmk_______", "tFolder_____", "tbBmk_______", "bzBmk_______", "mozBmk______", PlacesUtils.bookmarks.toolbarGuid, ]); observer.check([ { name: "bookmark-added", params: { itemId: localItemIds.get("fxBmk_______"), parentId: localItemIds.get(PlacesUtils.bookmarks.toolbarGuid), index: 0, type: PlacesUtils.bookmarks.TYPE_BOOKMARK, urlHref: "http://getfirefox.com/", title: "Get Firefox", guid: "fxBmk_______", parentGuid: PlacesUtils.bookmarks.toolbarGuid, source: PlacesUtils.bookmarks.SOURCES.SYNC, dateAdded: now, }, }, { name: "bookmark-added", params: { itemId: localItemIds.get("tFolder_____"), parentId: localItemIds.get(PlacesUtils.bookmarks.toolbarGuid), index: 1, type: PlacesUtils.bookmarks.TYPE_FOLDER, urlHref: "", title: "Mail", guid: "tFolder_____", parentGuid: PlacesUtils.bookmarks.toolbarGuid, source: PlacesUtils.bookmarks.SOURCES.SYNC, dateAdded: now, }, }, { name: "bookmark-added", params: { itemId: localItemIds.get("tbBmk_______"), parentId: localItemIds.get("tFolder_____"), index: 0, type: PlacesUtils.bookmarks.TYPE_BOOKMARK, urlHref: "http://getthunderbird.com/", title: "Get Thunderbird", guid: "tbBmk_______", parentGuid: "tFolder_____", source: PlacesUtils.bookmarks.SOURCES.SYNC, dateAdded: now, }, }, { name: "bookmark-moved", params: { itemId: localItemIds.get("bzBmk_______"), oldIndex: 0, newIndex: 2, type: PlacesUtils.bookmarks.TYPE_BOOKMARK, guid: "bzBmk_______", oldParentGuid: PlacesUtils.bookmarks.toolbarGuid, newParentGuid: PlacesUtils.bookmarks.toolbarGuid, source: PlacesUtils.bookmarks.SOURCES.SYNC, urlHref: "https://bugzilla.mozilla.org/", isTagging: false, }, }, { name: "bookmark-title-changed", params: { itemId: localItemIds.get("mozBmk______"), title: "Mozilla home page", guid: "mozBmk______", parentGuid: PlacesUtils.bookmarks.menuGuid, }, }, ]); let fxBmk = await PlacesUtils.bookmarks.fetch("fxBmk_______"); ok(fxBmk, "New Firefox bookmark should exist"); equal( fxBmk.parentGuid, PlacesUtils.bookmarks.toolbarGuid, "Should add Firefox bookmark to toolbar" ); let fxTags = PlacesUtils.tagging.getTagsForURI( Services.io.newURI("http://getfirefox.com") ); deepEqual( fxTags.sort(), ["browsers", "taggy"], "Should tag new Firefox bookmark" ); let folder = await PlacesUtils.bookmarks.fetch("tFolder_____"); ok(folder, "New folder should exist"); equal( folder.parentGuid, PlacesUtils.bookmarks.toolbarGuid, "Should add new folder to toolbar" ); let tbBmk = await PlacesUtils.bookmarks.fetch("tbBmk_______"); ok(tbBmk, "Should insert Thunderbird child bookmark"); equal( tbBmk.parentGuid, folder.guid, "Should add Thunderbird bookmark to new folder" ); let keywordInfo = await PlacesUtils.keywords.fetch("tb"); equal( keywordInfo.url.href, "http://getthunderbird.com/", "Should set keyword for Thunderbird bookmark" ); let updatedBmk = await PlacesUtils.bookmarks.fetch("mozBmk______"); equal( updatedBmk.title, "Mozilla home page", "Should rename Mozilla bookmark" ); equal( updatedBmk.parentGuid, PlacesUtils.bookmarks.menuGuid, "Should not move Mozilla bookmark" ); 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_value_only_changes() { let buf = await openMirror("value_only_changes"); info("Set up mirror"); await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.menuGuid, children: [ { guid: "folderAAAAAA", type: PlacesUtils.bookmarks.TYPE_FOLDER, title: "A", children: [ { guid: "bookmarkBBBB", url: "http://example.com/b", title: "B", }, { guid: "bookmarkCCCC", url: "http://example.com/c", title: "C", }, { guid: "folderJJJJJJ", type: PlacesUtils.bookmarks.TYPE_FOLDER, title: "J", children: [ { guid: "bookmarkKKKK", url: "http://example.com/k", title: "K", }, ], }, { guid: "bookmarkDDDD", url: "http://example.com/d", title: "D", }, { guid: "bookmarkEEEE", url: "http://example.com/e", title: "E", }, ], }, { guid: "folderFFFFFF", type: PlacesUtils.bookmarks.TYPE_FOLDER, title: "F", children: [ { guid: "bookmarkGGGG", url: "http://example.com/g", title: "G", }, { guid: "folderHHHHHH", type: PlacesUtils.bookmarks.TYPE_FOLDER, title: "H", children: [ { guid: "bookmarkIIII", url: "http://example.com/i", title: "I", }, ], }, ], }, ], }); await storeRecords( buf, shuffle([ { id: "menu", parentid: "places", type: "folder", children: ["folderAAAAAA", "folderFFFFFF"], }, { id: "folderAAAAAA", parentid: "menu", type: "folder", title: "A", children: [ "bookmarkBBBB", "bookmarkCCCC", "folderJJJJJJ", "bookmarkDDDD", "bookmarkEEEE", ], }, { id: "bookmarkBBBB", parentid: "folderAAAAAA", type: "bookmark", title: "B", bmkUri: "http://example.com/b", }, { id: "bookmarkCCCC", parentid: "folderAAAAAA", type: "bookmark", title: "C", bmkUri: "http://example.com/c", }, { id: "folderJJJJJJ", parentid: "folderAAAAAA", type: "folder", title: "J", children: ["bookmarkKKKK"], }, { id: "bookmarkKKKK", parentid: "folderJJJJJJ", type: "bookmark", title: "K", bmkUri: "http://example.com/k", }, { id: "bookmarkDDDD", parentid: "folderAAAAAA", type: "bookmark", title: "D", bmkUri: "http://example.com/d", }, { id: "bookmarkEEEE", parentid: "folderAAAAAA", type: "bookmark", title: "E", bmkUri: "http://example.com/e", }, { id: "folderFFFFFF", parentid: "menu", type: "folder", title: "F", children: ["bookmarkGGGG", "folderHHHHHH"], }, { id: "bookmarkGGGG", parentid: "folderFFFFFF", type: "bookmark", title: "G", bmkUri: "http://example.com/g", }, { id: "folderHHHHHH", parentid: "folderFFFFFF", type: "folder", title: "H", children: ["bookmarkIIII"], }, { id: "bookmarkIIII", parentid: "folderHHHHHH", type: "bookmark", title: "I", bmkUri: "http://example.com/i", }, ]), { needsMerge: false } ); await PlacesTestUtils.markBookmarksAsSynced(); info("Make remote changes"); await storeRecords( buf, shuffle([ { id: "bookmarkCCCC", parentid: "folderAAAAAA", type: "bookmark", title: "C (remote)", bmkUri: "http://example.com/c-remote", }, { id: "bookmarkEEEE", parentid: "folderAAAAAA", type: "bookmark", title: "E (remote)", bmkUri: "http://example.com/e-remote", }, { id: "bookmarkIIII", parentid: "folderHHHHHH", type: "bookmark", title: "I (remote)", bmkUri: "http://example.com/i-remote", }, { id: "folderFFFFFF", parentid: "menu", type: "folder", title: "F (remote)", children: ["bookmarkGGGG", "folderHHHHHH"], }, ]) ); info("Apply remote"); let changesToUpload = await buf.apply(); let idsToUpload = inspectChangeRecords(changesToUpload); deepEqual( idsToUpload, { updated: [], deleted: [], }, "Should not upload records for remote-only value changes" ); 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: "bookmarkCCCC", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, index: 1, title: "C (remote)", url: "http://example.com/c-remote", }, { guid: "folderJJJJJJ", type: PlacesUtils.bookmarks.TYPE_FOLDER, index: 2, title: "J", children: [ { guid: "bookmarkKKKK", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, index: 0, title: "K", url: "http://example.com/k", }, ], }, { guid: "bookmarkDDDD", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, index: 3, title: "D", url: "http://example.com/d", }, { guid: "bookmarkEEEE", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, index: 4, title: "E (remote)", url: "http://example.com/e-remote", }, ], }, { guid: "folderFFFFFF", type: PlacesUtils.bookmarks.TYPE_FOLDER, index: 1, title: "F (remote)", children: [ { guid: "bookmarkGGGG", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, index: 0, title: "G", url: "http://example.com/g", }, { guid: "folderHHHHHH", type: PlacesUtils.bookmarks.TYPE_FOLDER, index: 1, title: "H", children: [ { guid: "bookmarkIIII", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, index: 0, title: "I (remote)", url: "http://example.com/i-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 not change structure for value-only changes" ); 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_conflicting_keywords() { let buf = await openMirror("conflicting_keywords"); let dateAdded = new Date(); info("Set up mirror"); await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.menuGuid, children: [ { guid: "bookmarkAAAA", title: "A", url: "http://example.com/a", keyword: "one", dateAdded, }, ], }); await storeRecords( buf, shuffle([ { id: "menu", parentid: "places", type: "folder", children: ["bookmarkAAAA"], }, { id: "bookmarkAAAA", parentid: "menu", type: "bookmark", title: "A", bmkUri: "http://example.com/a", keyword: "one", dateAdded: dateAdded.getTime(), }, ]), { needsMerge: false } ); await PlacesTestUtils.markBookmarksAsSynced(); { let entryByKeyword = await PlacesUtils.keywords.fetch("one"); equal( entryByKeyword.url.href, "http://example.com/a", "Should return new keyword entry by URL" ); let entryByURL = await PlacesUtils.keywords.fetch({ url: "http://example.com/a", }); equal(entryByURL.keyword, "one", "Should return new entry by keyword"); } info("Insert new bookmark with same URL and different keyword"); { await storeRecords( buf, shuffle([ { id: "toolbar", parentid: "places", type: "folder", children: ["bookmarkAAA1"], }, { id: "bookmarkAAA1", parentid: "toolbar", type: "bookmark", title: "A1", bmkUri: "http://example.com/a", keyword: "two", dateAdded: dateAdded.getTime(), }, ]) ); let changesToUpload = await buf.apply(); deepEqual( await buf.fetchUnmergedGuids(), ["bookmarkAAA1"], "Should leave A1 with conflicting keyword unmerged" ); 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", keyword: "two", }, }, bookmarkAAA1: { tombstone: false, counter: 1, synced: false, cleartext: { id: "bookmarkAAA1", type: "bookmark", parentid: "toolbar", hasDupe: true, parentName: BookmarksToolbarTitle, dateAdded: dateAdded.getTime(), bmkUri: "http://example.com/a", title: "A1", keyword: "two", }, }, }, "Should reupload bookmarks with different keyword" ); await storeChangesInMirror(buf, changesToUpload); let entryByOldKeyword = await PlacesUtils.keywords.fetch("one"); ok( !entryByOldKeyword, "Should remove old entry when inserting bookmark with different keyword" ); let entryByNewKeyword = await PlacesUtils.keywords.fetch("two"); equal( entryByNewKeyword.url.href, "http://example.com/a", "Should return new keyword entry by URL" ); let entryByURL = await PlacesUtils.keywords.fetch({ url: "http://example.com/a", }); equal(entryByURL.keyword, "two", "Should return new entry by URL"); } info("Update bookmark with different keyword"); { await storeRecords( buf, shuffle([ { id: "bookmarkAAAA", parentid: "menu", type: "bookmark", title: "A", bmkUri: "http://example.com/a", keyword: "three", dateAdded: dateAdded.getTime(), }, ]) ); let changesToUpload = await buf.apply(); deepEqual( await buf.fetchUnmergedGuids(), ["bookmarkAAAA"], "Should leave A with conflicting keyword unmerged" ); 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", keyword: "three", }, }, bookmarkAAA1: { tombstone: false, counter: 1, synced: false, cleartext: { id: "bookmarkAAA1", type: "bookmark", parentid: "toolbar", hasDupe: true, parentName: BookmarksToolbarTitle, dateAdded: dateAdded.getTime(), bmkUri: "http://example.com/a", title: "A1", keyword: "three", }, }, }, "Should reupload A and A1 with updated keyword" ); await storeChangesInMirror(buf, changesToUpload); let entryByOldKeyword = await PlacesUtils.keywords.fetch("two"); ok( !entryByOldKeyword, "Should remove old entry when updating bookmark keyword" ); let entryByNewKeyword = await PlacesUtils.keywords.fetch("three"); equal( entryByNewKeyword.url.href, "http://example.com/a", "Should return updated keyword entry by URL" ); let entryByURL = await PlacesUtils.keywords.fetch({ url: "http://example.com/a", }); equal(entryByURL.keyword, "three", "Should return updated entry by URL"); } await buf.finalize(); await PlacesUtils.bookmarks.eraseEverything(); await PlacesSyncUtils.bookmarks.reset(); }); add_task(async function test_keywords() { let buf = await openMirror("keywords"); let now = new Date(); info("Set up mirror"); await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.menuGuid, children: [ { guid: "bookmarkAAAA", title: "A", url: "http://example.com/a", keyword: "one", dateAdded: now, }, { guid: "bookmarkBBBB", title: "B", url: "http://example.com/b", keyword: "two", dateAdded: now, }, { guid: "bookmarkCCCC", title: "C", url: "http://example.com/c", dateAdded: now, }, { guid: "bookmarkDDDD", title: "D", url: "http://example.com/d", keyword: "three", dateAdded: now, }, ], }); await storeRecords( buf, shuffle([ { id: "menu", parentid: "places", type: "folder", children: [ "bookmarkAAAA", "bookmarkBBBB", "bookmarkCCCC", "bookmarkDDDD", ], }, { id: "bookmarkAAAA", parentid: "menu", type: "bookmark", title: "A", bmkUri: "http://example.com/a", keyword: "one", dateAdded: now.getTime(), }, { id: "bookmarkBBBB", parentid: "menu", type: "bookmark", title: "B", bmkUri: "http://example.com/b", keyword: "two", dateAdded: now.getTime(), }, { id: "bookmarkCCCC", parentid: "menu", type: "bookmark", title: "C", bmkUri: "http://example.com/c", dateAdded: now.getTime(), }, { id: "bookmarkDDDD", parentid: "menu", type: "bookmark", title: "D", bmkUri: "http://example.com/d", keyword: "three", dateAdded: now.getTime(), }, ]), { needsMerge: false } ); await PlacesTestUtils.markBookmarksAsSynced(); info("Change keywords remotely"); await storeRecords( buf, shuffle([ { id: "bookmarkAAAA", parentid: "menu", type: "bookmark", title: "A", bmkUri: "http://example.com/a", keyword: "two", dateAdded: now.getTime(), }, { id: "bookmarkBBBB", parentid: "menu", type: "bookmark", title: "B", bmkUri: "http://example.com/b", dateAdded: now.getTime(), }, ]) ); info("Change keywords locally"); await PlacesUtils.keywords.insert({ keyword: "four", url: "http://example.com/c", }); await PlacesUtils.keywords.remove("three"); info("Apply remote"); let changesToUpload = await buf.apply(); deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items"); deepEqual( changesToUpload, { bookmarkCCCC: { tombstone: false, counter: 1, synced: false, cleartext: { id: "bookmarkCCCC", type: "bookmark", parentid: "menu", hasDupe: true, parentName: BookmarksMenuTitle, dateAdded: now.getTime(), bmkUri: "http://example.com/c", title: "C", keyword: "four", }, }, bookmarkDDDD: { tombstone: false, counter: 1, synced: false, cleartext: { id: "bookmarkDDDD", type: "bookmark", parentid: "menu", hasDupe: true, parentName: BookmarksMenuTitle, dateAdded: now.getTime(), bmkUri: "http://example.com/d", title: "D", }, }, }, "Should upload C with new keyword, D with keyword removed" ); let entryForOne = await PlacesUtils.keywords.fetch("one"); ok(!entryForOne, "Should remove existing keyword from A"); let entriesForTwo = await fetchAllKeywords("two"); deepEqual( entriesForTwo.map(entry => ({ keyword: entry.keyword, url: entry.url.href, })), [ { keyword: "two", url: "http://example.com/a", }, ], "Should move keyword for B to A" ); await buf.finalize(); await PlacesUtils.bookmarks.eraseEverything(); await PlacesSyncUtils.bookmarks.reset(); }); add_task(async function test_keywords_complex() { let buf = await openMirror("keywords_complex"); let now = new Date(); info("Set up mirror"); await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.menuGuid, children: [ { guid: "bookmarkBBBB", title: "B", url: "http://example.com/b", keyword: "four", dateAdded: now, }, { guid: "bookmarkCCCC", title: "C", url: "http://example.com/c", keyword: "five", dateAdded: now, }, { guid: "bookmarkDDDD", title: "D", url: "http://example.com/d", dateAdded: now, }, { guid: "bookmarkEEEE", title: "E", url: "http://example.com/e", keyword: "three", dateAdded: now, }, ], }); await storeRecords( buf, shuffle([ { id: "menu", parentid: "places", type: "folder", children: [ "bookmarkBBBB", "bookmarkCCCC", "bookmarkDDDD", "bookmarkEEEE", ], }, { id: "bookmarkBBBB", parentid: "menu", type: "bookmark", title: "B", bmkUri: "http://example.com/b", keyword: "four", dateAdded: now.getTime(), }, { id: "bookmarkCCCC", parentid: "menu", type: "bookmark", title: "C", bmkUri: "http://example.com/c", keyword: "five", dateAdded: now.getTime(), }, { id: "bookmarkDDDD", parentid: "menu", type: "bookmark", title: "D", bmkUri: "http://example.com/d", dateAdded: now.getTime(), }, { id: "bookmarkEEEE", parentid: "menu", type: "bookmark", title: "E", bmkUri: "http://example.com/e", keyword: "three", dateAdded: now.getTime(), }, ]), { needsMerge: false } ); await PlacesTestUtils.markBookmarksAsSynced(); info("Make remote changes"); await storeRecords( buf, shuffle([ { id: "menu", parentid: "places", type: "folder", children: [ "bookmarkAAAA", "bookmarkAAA1", "bookmarkBBB1", "bookmarkBBBB", "bookmarkCCCC", "bookmarkDDDD", "bookmarkEEEE", ], }, { id: "bookmarkAAAA", parentid: "menu", type: "bookmark", title: "A", bmkUri: "http://example.com/a", keyword: "one", dateAdded: now.getTime(), }, { id: "bookmarkAAA1", parentid: "menu", type: "bookmark", title: "A (copy)", bmkUri: "http://example.com/a", keyword: "two", dateAdded: now.getTime(), }, { id: "bookmarkBBB1", parentid: "menu", type: "bookmark", title: "B", bmkUri: "http://example.com/b", dateAdded: now.getTime(), }, { id: "bookmarkCCCC", parentid: "menu", type: "bookmark", title: "C (remote)", bmkUri: "http://example.com/c-remote", keyword: "six", dateAdded: now.getTime(), }, ]) ); info("Apply remote"); let observer = expectBookmarkChangeNotifications(); let changesToUpload = await buf.apply({ notifyInStableOrder: true, }); deepEqual( await buf.fetchUnmergedGuids(), ["bookmarkAAA1", "bookmarkAAAA", "bookmarkBBB1"], "Should leave A1, A, B with conflicting keywords unmerged" ); let expectedChangesToUpload = { bookmarkBBBB: { tombstone: false, counter: 1, synced: false, cleartext: { id: "bookmarkBBBB", type: "bookmark", parentid: "menu", hasDupe: true, parentName: BookmarksMenuTitle, dateAdded: now.getTime(), bmkUri: "http://example.com/b", title: "B", }, }, bookmarkBBB1: { tombstone: false, counter: 1, synced: false, cleartext: { id: "bookmarkBBB1", type: "bookmark", parentid: "menu", hasDupe: true, parentName: BookmarksMenuTitle, dateAdded: now.getTime(), bmkUri: "http://example.com/b", title: "B", }, }, bookmarkAAAA: { tombstone: false, counter: 1, synced: false, cleartext: { id: "bookmarkAAAA", type: "bookmark", parentid: "menu", hasDupe: true, parentName: BookmarksMenuTitle, dateAdded: now.getTime(), bmkUri: "http://example.com/a", title: "A", }, }, bookmarkAAA1: { tombstone: false, counter: 1, synced: false, cleartext: { id: "bookmarkAAA1", type: "bookmark", parentid: "menu", hasDupe: true, parentName: BookmarksMenuTitle, dateAdded: now.getTime(), bmkUri: "http://example.com/a", title: "A (copy)", }, }, }; // We'll take the keyword of either "bookmarkAAAA" or "bookmarkAAA1", // depending on which we see first, and reupload the other. let entriesForOne = await fetchAllKeywords("one"); let entriesForTwo = await fetchAllKeywords("two"); if (entriesForOne.length) { ok(!entriesForTwo.length, "Should drop conflicting keyword from A1"); deepEqual( entriesForOne.map(keyword => keyword.url.href), ["http://example.com/a"], "Should use A keyword for A and A1" ); expectedChangesToUpload.bookmarkAAAA.cleartext.keyword = "one"; expectedChangesToUpload.bookmarkAAA1.cleartext.keyword = "one"; } else { ok(!entriesForOne.length, "Should drop conflicting keyword from A"); deepEqual( entriesForTwo.map(keyword => keyword.url.href), ["http://example.com/a"], "Should use A1 keyword for A and A1" ); expectedChangesToUpload.bookmarkAAAA.cleartext.keyword = "two"; expectedChangesToUpload.bookmarkAAA1.cleartext.keyword = "two"; } deepEqual( changesToUpload, expectedChangesToUpload, "Should reupload all local records with corrected keywords" ); let localItemIds = await PlacesUtils.promiseManyItemIds([ "bookmarkAAAA", "bookmarkAAA1", "bookmarkBBB1", "bookmarkBBBB", "bookmarkCCCC", "bookmarkDDDD", "bookmarkEEEE", PlacesUtils.bookmarks.menuGuid, ]); let expectedNotifications = [ { name: "bookmark-added", params: { itemId: localItemIds.get("bookmarkAAAA"), parentId: localItemIds.get(PlacesUtils.bookmarks.menuGuid), index: 0, type: PlacesUtils.bookmarks.TYPE_BOOKMARK, urlHref: "http://example.com/a", title: "A", guid: "bookmarkAAAA", parentGuid: PlacesUtils.bookmarks.menuGuid, source: PlacesUtils.bookmarks.SOURCES.SYNC, }, }, { name: "bookmark-added", params: { itemId: localItemIds.get("bookmarkAAA1"), parentId: localItemIds.get(PlacesUtils.bookmarks.menuGuid), index: 1, type: PlacesUtils.bookmarks.TYPE_BOOKMARK, urlHref: "http://example.com/a", title: "A (copy)", guid: "bookmarkAAA1", parentGuid: PlacesUtils.bookmarks.menuGuid, source: PlacesUtils.bookmarks.SOURCES.SYNC, }, }, { name: "bookmark-added", params: { itemId: localItemIds.get("bookmarkBBB1"), parentId: localItemIds.get(PlacesUtils.bookmarks.menuGuid), index: 2, type: PlacesUtils.bookmarks.TYPE_BOOKMARK, urlHref: "http://example.com/b", title: "B", guid: "bookmarkBBB1", parentGuid: PlacesUtils.bookmarks.menuGuid, source: PlacesUtils.bookmarks.SOURCES.SYNC, }, }, { // These `bookmark-moved` notifications aren't necessary: we only moved // (B C D E) to accomodate (A A1 B1), and Places doesn't usually fire move // notifications for repositioned siblings. However, detecting and filtering // these out complicates `noteObserverChanges`, so, for simplicity, we // record and fire the extra notifications. name: "bookmark-moved", params: { itemId: localItemIds.get("bookmarkBBBB"), oldIndex: 0, newIndex: 3, type: PlacesUtils.bookmarks.TYPE_BOOKMARK, guid: "bookmarkBBBB", oldParentGuid: PlacesUtils.bookmarks.menuGuid, newParentGuid: PlacesUtils.bookmarks.menuGuid, source: PlacesUtils.bookmarks.SOURCES.SYNC, urlHref: "http://example.com/b", isTagging: false, }, }, { name: "bookmark-moved", params: { itemId: localItemIds.get("bookmarkCCCC"), oldIndex: 1, newIndex: 4, type: PlacesUtils.bookmarks.TYPE_BOOKMARK, guid: "bookmarkCCCC", oldParentGuid: PlacesUtils.bookmarks.menuGuid, newParentGuid: PlacesUtils.bookmarks.menuGuid, source: PlacesUtils.bookmarks.SOURCES.SYNC, urlHref: "http://example.com/c-remote", isTagging: false, }, }, { name: "bookmark-moved", params: { itemId: localItemIds.get("bookmarkDDDD"), oldIndex: 2, newIndex: 5, type: PlacesUtils.bookmarks.TYPE_BOOKMARK, guid: "bookmarkDDDD", oldParentGuid: PlacesUtils.bookmarks.menuGuid, newParentGuid: PlacesUtils.bookmarks.menuGuid, source: PlacesUtils.bookmarks.SOURCES.SYNC, urlHref: "http://example.com/d", isTagging: false, }, }, { name: "bookmark-moved", params: { itemId: localItemIds.get("bookmarkEEEE"), oldIndex: 3, newIndex: 6, type: PlacesUtils.bookmarks.TYPE_BOOKMARK, guid: "bookmarkEEEE", oldParentGuid: PlacesUtils.bookmarks.menuGuid, newParentGuid: PlacesUtils.bookmarks.menuGuid, source: PlacesUtils.bookmarks.SOURCES.SYNC, urlHref: "http://example.com/e", isTagging: false, }, }, { name: "bookmark-title-changed", params: { itemId: localItemIds.get("bookmarkCCCC"), title: "C (remote)", guid: "bookmarkCCCC", parentGuid: PlacesUtils.bookmarks.menuGuid, }, }, { name: "bookmark-url-changed", params: { itemId: localItemIds.get("bookmarkCCCC"), type: PlacesUtils.bookmarks.TYPE_BOOKMARK, urlHref: "http://example.com/c-remote", guid: "bookmarkCCCC", parentGuid: PlacesUtils.bookmarks.menuGuid, source: PlacesUtils.bookmarks.SOURCES.SYNC, isTagging: false, }, }, ]; observer.check(expectedNotifications); let entriesForFour = await fetchAllKeywords("four"); ok(!entriesForFour.length, "Should remove all keywords for B"); let entriesForOldC = await fetchAllKeywords({ url: "http://example.com/c", }); ok(!entriesForOldC.length, "Should remove all keywords from old C URL"); let entriesForNewC = await fetchAllKeywords({ url: "http://example.com/c-remote", }); deepEqual( entriesForNewC.map(entry => entry.keyword), ["six"], "Should add new keyword to new C URL" ); let entriesForD = await fetchAllKeywords("http://example.com/d"); ok(!entriesForD.length, "Should not add keywords to D"); let entriesForThree = await fetchAllKeywords("three"); deepEqual( entriesForThree.map(keyword => keyword.url.href), ["http://example.com/e"], "Should not change keywords for E" ); await buf.finalize(); await PlacesUtils.bookmarks.eraseEverything(); await PlacesSyncUtils.bookmarks.reset(); }); add_task(async function test_tags_complex() { let buf = await openMirror("tags_complex"); info("Set up mirror"); await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.menuGuid, children: [ { guid: "bookmarkAAA1", title: "A1", url: "http://example.com/a", tags: ["one", "two"], }, { guid: "bookmarkAAA2", title: "A2", url: "http://example.com/a", tags: ["one", "two"], }, { guid: "bookmarkBBB1", title: "B1", url: "http://example.com/b", tags: ["one"], }, { guid: "bookmarkBBB2", title: "B2", url: "http://example.com/b", tags: ["one"], }, { guid: "bookmarkCCC1", title: "C1", url: "http://example.com/c", tags: ["two", "three"], }, { guid: "bookmarkCCC2", title: "C2", url: "http://example.com/c", tags: ["two", "three"], }, ], }); await storeRecords( buf, shuffle([ { id: "menu", parentid: "places", type: "folder", children: [ "bookmarkAAA1", "bookmarkAAA2", "bookmarkBBB1", "bookmarkBBB2", "bookmarkCCC1", "bookmarkCCC2", ], }, { id: "bookmarkAAA1", parentid: "menu", type: "bookmark", title: "A", bmkUri: "http://example.com/a", tags: ["one", "two"], }, { id: "bookmarkAAA2", parentid: "menu", type: "bookmark", title: "A", bmkUri: "http://example.com/a", tags: ["one", "two"], }, { id: "bookmarkBBB1", parentid: "menu", type: "bookmark", title: "B1", bmkUri: "http://example.com/b", tags: ["one"], }, { id: "bookmarkBBB2", parentid: "menu", type: "bookmark", title: "B2", bmkUri: "http://example.com/b", tags: ["one"], }, { id: "bookmarkCCC1", parentid: "menu", type: "bookmark", title: "C1", bmkUri: "http://example.com/c", tags: ["two", "three"], }, { id: "bookmarkCCC2", parentid: "menu", type: "bookmark", title: "C2", bmkUri: "http://example.com/c", tags: ["two", "three"], }, ]), { needsMerge: false } ); await PlacesTestUtils.markBookmarksAsSynced(); info("Add tags for B locally"); PlacesUtils.tagging.tagURI(Services.io.newURI("http://example.com/b"), [ "four", "five", ]); info("Remove tag from C locally"); PlacesUtils.tagging.untagURI(Services.io.newURI("http://example.com/c"), [ "two", ]); info("Update tags for A remotely"); await storeRecords( buf, shuffle([ { id: "bookmarkAAA1", parentid: "menu", type: "bookmark", title: "A1", bmkUri: "http://example.com/a", tags: ["one", "two", "four", "six"], }, { id: "bookmarkAAA2", parentid: "menu", type: "bookmark", title: "A2", bmkUri: "http://example.com/a", tags: ["one", "two", "four", "six"], }, ]) ); info("Apply remote"); let changesToUpload = await buf.apply(); let datesAdded = await promiseManyDatesAdded([ "bookmarkBBB1", "bookmarkBBB2", "bookmarkCCC1", "bookmarkCCC2", ]); deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items"); deepEqual( changesToUpload, { bookmarkBBB1: { counter: 2, synced: false, tombstone: false, cleartext: { id: "bookmarkBBB1", type: "bookmark", parentid: "menu", hasDupe: true, parentName: BookmarksMenuTitle, dateAdded: datesAdded.get("bookmarkBBB1"), bmkUri: "http://example.com/b", title: "B1", tags: ["five", "four", "one"], }, }, bookmarkBBB2: { counter: 2, synced: false, tombstone: false, cleartext: { id: "bookmarkBBB2", type: "bookmark", parentid: "menu", hasDupe: true, parentName: BookmarksMenuTitle, dateAdded: datesAdded.get("bookmarkBBB2"), bmkUri: "http://example.com/b", title: "B2", tags: ["five", "four", "one"], }, }, bookmarkCCC1: { counter: 1, synced: false, tombstone: false, cleartext: { id: "bookmarkCCC1", type: "bookmark", parentid: "menu", hasDupe: true, parentName: BookmarksMenuTitle, dateAdded: datesAdded.get("bookmarkCCC1"), bmkUri: "http://example.com/c", title: "C1", tags: ["three"], }, }, bookmarkCCC2: { counter: 1, synced: false, tombstone: false, cleartext: { id: "bookmarkCCC2", type: "bookmark", parentid: "menu", hasDupe: true, parentName: BookmarksMenuTitle, dateAdded: datesAdded.get("bookmarkCCC2"), bmkUri: "http://example.com/c", title: "C2", tags: ["three"], }, }, }, "Should upload local records with new tags" ); 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: "A1", url: "http://example.com/a", tags: ["four", "one", "six", "two"], }, { guid: "bookmarkAAA2", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, index: 1, title: "A2", url: "http://example.com/a", tags: ["four", "one", "six", "two"], }, { guid: "bookmarkBBB1", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, index: 2, title: "B1", url: "http://example.com/b", tags: ["five", "four", "one"], }, { guid: "bookmarkBBB2", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, index: 3, title: "B2", url: "http://example.com/b", tags: ["five", "four", "one"], }, { guid: "bookmarkCCC1", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, index: 4, title: "C1", url: "http://example.com/c", tags: ["three"], }, { guid: "bookmarkCCC2", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, index: 5, title: "C2", url: "http://example.com/c", tags: ["three"], }, ], }, "Should update local items with new tags" ); await buf.finalize(); await PlacesUtils.bookmarks.eraseEverything(); await PlacesSyncUtils.bookmarks.reset(); }); add_task(async function test_tags() { let buf = await openMirror("tags"); info("Set up mirror"); await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.menuGuid, children: [ { guid: "bookmarkAAAA", title: "A", url: "http://example.com/a", tags: ["one", "two", "three", "four"], }, { guid: "bookmarkBBBB", title: "B", url: "http://example.com/b", tags: ["five", "six"], }, { guid: "bookmarkCCCC", title: "C", url: "http://example.com/c", }, { guid: "bookmarkDDDD", title: "D", url: "http://example.com/d", tags: ["seven", "eight", "nine"], }, ], }); await storeRecords( buf, shuffle([ { id: "menu", parentid: "places", type: "folder", children: [ "bookmarkAAAA", "bookmarkBBBB", "bookmarkCCCC", "bookmarkDDDD", ], }, { id: "bookmarkAAAA", parentid: "menu", type: "bookmark", title: "A", bmkUri: "http://example.com/a", tags: ["one", "two", "three", "four"], }, { id: "bookmarkBBBB", parentid: "menu", type: "bookmark", title: "B", bmkUri: "http://example.com/b", tags: ["five", "six"], }, { id: "bookmarkCCCC", parentid: "menu", type: "bookmark", title: "C", bmkUri: "http://example.com/c", }, { id: "bookmarkDDDD", parentid: "menu", type: "bookmark", title: "D", bmkUri: "http://example.com/d", tags: ["seven", "eight", "nine"], }, ]), { needsMerge: false } ); await PlacesTestUtils.markBookmarksAsSynced(); info("Change tags remotely"); await storeRecords( buf, shuffle([ { id: "bookmarkAAAA", parentid: "menu", type: "bookmark", title: "A", bmkUri: "http://example.com/a", tags: ["one", "two", "ten"], }, { id: "bookmarkBBBB", parentid: "menu", type: "bookmark", title: "B", bmkUri: "http://example.com/b", tags: [], }, ]) ); info("Change tags locally"); PlacesUtils.tagging.tagURI(Services.io.newURI("http://example.com/c"), [ "eleven", "twelve", ]); let wait = PlacesTestUtils.waitForNotification("bookmark-removed", events => events.some(event => event.parentGuid == PlacesUtils.bookmarks.tagsGuid) ); PlacesUtils.tagging.untagURI( Services.io.newURI("http://example.com/d"), null ); await wait; await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); info("Apply remote"); let changesToUpload = await buf.apply(); deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items"); let idsToUpload = inspectChangeRecords(changesToUpload); deepEqual( idsToUpload, { updated: ["bookmarkCCCC", "bookmarkDDDD"], deleted: [], }, "Should upload local records with new tags" ); deepEqual( changesToUpload.bookmarkCCCC.cleartext.tags.sort(), ["eleven", "twelve"], "Should upload record with new tags for C" ); ok( !changesToUpload.bookmarkDDDD.cleartext.tags, "Should upload record for D with tags removed" ); let tagsForA = PlacesUtils.tagging.getTagsForURI( Services.io.newURI("http://example.com/a") ); deepEqual(tagsForA.sort(), ["one", "ten", "two"], "Should change tags for A"); let tagsForB = PlacesUtils.tagging.getTagsForURI( Services.io.newURI("http://example.com/b") ); deepEqual(tagsForB, [], "Should remove all tags from B"); await buf.finalize(); await PlacesUtils.bookmarks.eraseEverything(); await PlacesSyncUtils.bookmarks.reset(); }); add_task(async function test_rewrite_tag_queries() { let buf = await openMirror("rewrite_tag_queries"); info("Set up mirror"); await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.menuGuid, children: [ { guid: "bookmarkAAAA", title: "A", url: "http://example.com/a", }, { guid: "bookmarkDDDD", title: "D", url: "http://example.com/d", tags: ["kitty"], }, ], }); await storeRecords( buf, [ { id: "menu", parentid: "places", type: "folder", children: ["bookmarkAAAA", "bookmarkDDDD"], }, { id: "bookmarkAAAA", parentid: "menu", type: "bookmark", title: "A", bmkUri: "http://example.com/a", }, { id: "bookmarkDDDD", parentid: "menu", type: "bookmark", title: "D", bmkUri: "http://example.com/d", tags: ["kitty"], }, ], { needsMerge: false } ); await PlacesTestUtils.markBookmarksAsSynced(); info("Add tag queries for new and existing tags"); await storeRecords(buf, [ { id: "toolbar", parentid: "places", type: "folder", children: ["queryBBBBBBB", "queryCCCCCCC", "bookmarkEEEE"], }, { id: "queryBBBBBBB", parentid: "toolbar", type: "query", title: "Tagged stuff", bmkUri: "place:type=7&folder=999", folderName: "taggy", }, { id: "queryCCCCCCC", parentid: "toolbar", type: "query", title: "Cats", bmkUri: "place:type=7&folder=888", folderName: "kitty", }, { id: "bookmarkEEEE", parentid: "toolbar", type: "bookmark", title: "E", bmkUri: "http://example.com/e", tags: ["taggy"], }, ]); info("Apply remote"); let changesToUpload = await buf.apply(); deepEqual( await buf.fetchUnmergedGuids(), ["queryBBBBBBB", "queryCCCCCCC"], "Should leave rewritten queries unmerged" ); deepEqual( changesToUpload, { queryBBBBBBB: { tombstone: false, counter: 1, synced: false, cleartext: { id: "queryBBBBBBB", type: "query", parentid: "toolbar", hasDupe: true, parentName: BookmarksToolbarTitle, dateAdded: undefined, bmkUri: "place:tag=taggy", title: "Tagged stuff", folderName: "taggy", }, }, queryCCCCCCC: { tombstone: false, counter: 1, synced: false, cleartext: { id: "queryCCCCCCC", type: "query", parentid: "toolbar", hasDupe: true, parentName: BookmarksToolbarTitle, dateAdded: undefined, bmkUri: "place:tag=kitty", title: "Cats", folderName: "kitty", }, }, }, "Should reupload (E C) with rewritten URLs" ); let bmWithTaggy = await PlacesUtils.bookmarks.fetch({ tags: ["taggy"] }); equal( bmWithTaggy.url.href, "http://example.com/e", "Should insert bookmark with new tag" ); let bmWithKitty = await PlacesUtils.bookmarks.fetch({ tags: ["kitty"] }); equal( bmWithKitty.url.href, "http://example.com/d", "Should retain existing tag" ); let { root: toolbarContainer } = PlacesUtils.getFolderContents( PlacesUtils.bookmarks.toolbarGuid, false, true ); equal( toolbarContainer.childCount, 3, "Should add queries and bookmark to toolbar" ); let containerForB = PlacesUtils.asContainer(toolbarContainer.getChild(0)); containerForB.containerOpen = true; for (let i = 0; i < containerForB.childCount; ++i) { let child = containerForB.getChild(i); equal( child.uri, "http://example.com/e", `Rewritten tag query B should have tagged child node at ${i}` ); } containerForB.containerOpen = false; let containerForC = PlacesUtils.asContainer(toolbarContainer.getChild(1)); containerForC.containerOpen = true; for (let i = 0; i < containerForC.childCount; ++i) { let child = containerForC.getChild(i); equal( child.uri, "http://example.com/d", `Rewritten tag query C should have tagged child node at ${i}` ); } containerForC.containerOpen = false; toolbarContainer.containerOpen = false; 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_date_added() { let buf = await openMirror("date_added"); let aDateAdded = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000); let bDateAdded = new Date(); info("Set up mirror"); await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.menuGuid, children: [ { guid: "bookmarkAAAA", dateAdded: aDateAdded, title: "A", url: "http://example.com/a", }, { guid: "bookmarkBBBB", dateAdded: bDateAdded, title: "B", url: "http://example.com/b", }, ], }); await storeRecords( buf, [ { id: "menu", parentid: "places", type: "folder", children: ["bookmarkAAAA", "bookmarkBBBB"], }, { id: "bookmarkAAAA", parentid: "menu", type: "bookmark", title: "A", dateAdded: aDateAdded.getTime(), bmkUri: "http://example.com/a", }, { id: "bookmarkBBBB", parentid: "menu", type: "bookmark", title: "B", dateAdded: bDateAdded.getTime(), bmkUri: "http://example.com/b", }, ], { needsMerge: false } ); await PlacesTestUtils.markBookmarksAsSynced(); info("Make remote changes"); let bNewDateAdded = new Date(bDateAdded.getTime() - 1 * 60 * 60 * 1000); await storeRecords(buf, [ { id: "bookmarkAAAA", parentid: "menu", type: "bookmark", title: "A (remote)", dateAdded: Date.now(), bmkUri: "http://example.com/a", }, { id: "bookmarkBBBB", parentid: "menu", type: "bookmark", title: "B (remote)", dateAdded: bNewDateAdded.getTime(), bmkUri: "http://example.com/b", }, ]); info("Apply remote"); let observer = expectBookmarkChangeNotifications(); let changesToUpload = await buf.apply({ notifyInStableOrder: true, }); deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items"); let idsToUpload = inspectChangeRecords(changesToUpload); deepEqual( idsToUpload, { updated: ["bookmarkAAAA"], deleted: [], }, "Should flag A for weak reupload" ); let localItemIds = await PlacesUtils.promiseManyItemIds([ "bookmarkAAAA", "bookmarkBBBB", ]); observer.check([ { name: "bookmark-title-changed", params: { itemId: localItemIds.get("bookmarkAAAA"), title: "A (remote)", guid: "bookmarkAAAA", parentGuid: PlacesUtils.bookmarks.menuGuid, }, }, { name: "bookmark-title-changed", params: { itemId: localItemIds.get("bookmarkBBBB"), title: "B (remote)", guid: "bookmarkBBBB", parentGuid: PlacesUtils.bookmarks.menuGuid, }, }, ]); let changeCounter = changesToUpload.bookmarkAAAA.counter; strictEqual(changeCounter, 0, "Should not bump change counter for A"); let aInfo = await PlacesUtils.bookmarks.fetch("bookmarkAAAA"); equal(aInfo.title, "A (remote)", "Should change local title for A"); deepEqual( aInfo.dateAdded, aDateAdded, "Should not change date added for A to newer remote date" ); let bInfo = await PlacesUtils.bookmarks.fetch("bookmarkBBBB"); equal(bInfo.title, "B (remote)", "Should change local title for B"); deepEqual( bInfo.dateAdded, bNewDateAdded, "Should take older date added for B" ); await buf.finalize(); await PlacesUtils.bookmarks.eraseEverything(); await PlacesSyncUtils.bookmarks.reset(); }); // Bug 1472435. add_task(async function test_duplicate_url_rows() { let buf = await openMirror("test_duplicate_url_rows"); let placesToInsert = [ { guid: "placeAAAAAAA", href: "http://example.com", }, { guid: "placeBBBBBBB", href: "http://example.com", }, { guid: "placeCCCCCCC", href: "http://example.com/c", }, ]; let itemsToInsert = [ { guid: "bookmarkAAAA", parentGuid: PlacesUtils.bookmarks.menuGuid, placeGuid: "placeAAAAAAA", localTitle: "A", remoteTitle: "A (remote)", }, { guid: "bookmarkBBBB", parentGuid: PlacesUtils.bookmarks.toolbarGuid, placeGuid: "placeBBBBBBB", localTitle: "B", remoteTitle: "B (remote)", }, { guid: "bookmarkCCCC", parentGuid: PlacesUtils.bookmarks.unfiledGuid, placeGuid: "placeCCCCCCC", localTitle: "C", remoteTitle: "C (remote)", }, ]; info("Manually insert local and remote items with duplicate URLs"); await buf.db.executeTransaction(async function () { for (let { guid, href } of placesToInsert) { let url = new URL(href); await buf.db.executeCached( ` INSERT INTO moz_places(url, url_hash, rev_host, hidden, frecency, guid) VALUES(:url, hash(:url), :revHost, 0, -1, :guid)`, { url: url.href, revHost: PlacesUtils.getReversedHost(url), guid } ); await buf.db.executeCached( ` INSERT INTO urls(guid, url, hash, revHost) VALUES(:guid, :url, hash(:url), :revHost)`, { guid, url: url.href, revHost: PlacesUtils.getReversedHost(url) } ); } for (let { guid, parentGuid, placeGuid, localTitle, remoteTitle, } of itemsToInsert) { await buf.db.executeCached( ` INSERT INTO moz_bookmarks(guid, parent, fk, position, type, title, syncStatus, syncChangeCounter) VALUES(:guid, (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid), (SELECT id FROM moz_places WHERE guid = :placeGuid), (SELECT count(*) FROM moz_bookmarks b JOIN moz_bookmarks p ON p.id = b.parent WHERE p.guid = :parentGuid), :type, :localTitle, :syncStatus, 1)`, { guid, parentGuid, placeGuid, type: PlacesUtils.bookmarks.TYPE_BOOKMARK, localTitle, syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NEW, } ); await buf.db.executeCached( ` INSERT INTO items(guid, parentGuid, needsMerge, kind, title, urlId) VALUES(:guid, :parentGuid, 1, :kind, :remoteTitle, (SELECT id FROM urls WHERE guid = :placeGuid))`, { guid, parentGuid, placeGuid, kind: Ci.mozISyncedBookmarksMerger.KIND_BOOKMARK, remoteTitle, } ); await buf.db.executeCached( ` INSERT INTO structure(guid, parentGuid, position) VALUES(:guid, :parentGuid, IFNULL((SELECT count(*) FROM structure WHERE parentGuid = :parentGuid), 0))`, { guid, parentGuid } ); } }); info("Apply mirror"); let observer = expectBookmarkChangeNotifications(); let changesToUpload = await buf.apply({ notifyInStableOrder: true, }); deepEqual( await buf.fetchUnmergedGuids(), [ PlacesUtils.bookmarks.menuGuid, PlacesUtils.bookmarks.mobileGuid, PlacesUtils.bookmarks.toolbarGuid, PlacesUtils.bookmarks.unfiledGuid, ], "Should leave roots unmerged" ); deepEqual( Object.keys(changesToUpload).sort(), ["menu", "mobile", "toolbar", "unfiled"], "Should upload 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 (remote)", url: "http://example.com/", }, ], }, { 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 (remote)", url: "http://example.com/", }, ], }, { guid: PlacesUtils.bookmarks.unfiledGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, index: 3, title: UnfiledBookmarksTitle, children: [ { guid: "bookmarkCCCC", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, index: 0, title: "C (remote)", url: "http://example.com/c", }, ], }, { guid: PlacesUtils.bookmarks.mobileGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, index: 4, title: MobileBookmarksTitle, }, ], }, "Should update titles for items with duplicate URLs" ); let localItemIds = await PlacesUtils.promiseManyItemIds([ "bookmarkAAAA", "bookmarkBBBB", "bookmarkCCCC", ]); observer.check([ { name: "bookmark-title-changed", params: { itemId: localItemIds.get("bookmarkAAAA"), title: "A (remote)", guid: "bookmarkAAAA", parentGuid: PlacesUtils.bookmarks.menuGuid, }, }, { name: "bookmark-title-changed", params: { itemId: localItemIds.get("bookmarkBBBB"), title: "B (remote)", guid: "bookmarkBBBB", parentGuid: PlacesUtils.bookmarks.toolbarGuid, }, }, { name: "bookmark-title-changed", params: { itemId: localItemIds.get("bookmarkCCCC"), title: "C (remote)", guid: "bookmarkCCCC", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }, }, ]); info("Remove duplicate URLs from Places to avoid tripping debug asserts"); await buf.db.executeTransaction(async function () { for (let { guid } of placesToInsert) { await buf.db.executeCached( ` DELETE FROM moz_places WHERE guid = :guid`, { 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_duplicate_local_tags() { let buf = await openMirror("duplicate_local_tags"); let now = new Date(); info("Insert A"); await PlacesUtils.bookmarks.insert({ guid: "bookmarkAAAA", parentGuid: PlacesUtils.bookmarks.menuGuid, title: "A", url: "http://example.com/a", dateAdded: now, }); // Each tag folder should have unique tag entries, but the tagging service // doesn't enforce this. We should still sync the correct set of tags, // though, even if there are duplicates for the same URL. info("Manually insert local tags for A"); for (let [tag, dupes] of [ ["one", 2], ["two", 1], ["three", 2], ]) { let tagFolderInfo = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.tagsGuid, title: tag, type: PlacesUtils.bookmarks.TYPE_FOLDER, }); for (let i = 0; i < dupes; ++i) { await PlacesUtils.bookmarks.insert({ parentGuid: tagFolderInfo.guid, url: "http://example.com/a", }); } } let tagsForA = PlacesUtils.tagging.getTagsForURI( Services.io.newURI("http://example.com/a") ); deepEqual( tagsForA.sort(), ["one", "one", "three", "three", "two"], "Tagging service should return duplicate tags" ); info("Apply remote"); let changesToUpload = await buf.apply(); deepEqual( changesToUpload.bookmarkAAAA.cleartext, { id: "bookmarkAAAA", type: "bookmark", parentid: "menu", hasDupe: true, parentName: BookmarksMenuTitle, dateAdded: now.getTime(), bmkUri: "http://example.com/a", title: "A", tags: ["one", "three", "two"], }, "Should upload A with tags" ); await buf.finalize(); await PlacesUtils.bookmarks.eraseEverything(); await PlacesSyncUtils.bookmarks.reset(); });