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