| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/sync_bookmarks/bookmark_remote_updates_handler.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/guid.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "components/bookmarks/browser/bookmark_model.h" |
| #include "components/bookmarks/test/test_bookmark_client.h" |
| #include "components/favicon/core/test/mock_favicon_service.h" |
| #include "components/sync/base/client_tag_hash.h" |
| #include "components/sync/base/hash_util.h" |
| #include "components/sync/base/model_type.h" |
| #include "components/sync/base/unique_position.h" |
| #include "components/sync/model/conflict_resolution.h" |
| #include "components/sync/protocol/bookmark_model_metadata.pb.h" |
| #include "components/sync/protocol/bookmark_specifics.pb.h" |
| #include "components/sync/protocol/entity_specifics.pb.h" |
| #include "components/sync/protocol/model_type_state.pb.h" |
| #include "components/sync/protocol/unique_position.pb.h" |
| #include "components/sync_bookmarks/bookmark_model_merger.h" |
| #include "components/sync_bookmarks/bookmark_specifics_conversions.h" |
| #include "components/sync_bookmarks/switches.h" |
| #include "components/sync_bookmarks/synced_bookmark_tracker_entity.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using base::ASCIIToUTF16; |
| using testing::_; |
| using testing::AnyOf; |
| using testing::ElementsAre; |
| using testing::Eq; |
| using testing::IsNull; |
| using testing::NotNull; |
| |
| namespace sync_bookmarks { |
| |
| namespace { |
| |
| // The parent tag for children of the root entity. Entities with this parent are |
| // referred to as top level entities. |
| const char kBookmarkBarId[] = "bookmark_bar_id"; |
| const char kBookmarkBarTag[] = "bookmark_bar"; |
| const char kMobileBookmarksId[] = "synced_bookmarks_id"; |
| const char kMobileBookmarksTag[] = "synced_bookmarks"; |
| const char kOtherBookmarksId[] = "other_bookmarks_id"; |
| const char kOtherBookmarksTag[] = "other_bookmarks"; |
| |
| // Fork of enum RemoteBookmarkUpdateError. |
| enum class ExpectedRemoteBookmarkUpdateError { |
| kConflictingTypes = 0, |
| kInvalidSpecifics = 1, |
| // kDeprecatedInvalidUniquePosition = 2, |
| // kDeprecatedPermanentNodeCreationAfterMerge = 3, |
| kMissingParentEntity = 4, |
| kMissingParentNode = 5, |
| kMissingParentEntityInConflict = 6, |
| kMissingParentNodeInConflict = 7, |
| kCreationFailure = 8, |
| kUnexpectedGuid = 9, |
| kParentNotFolder = 10, |
| kGuidChangedForTrackedServerId = 11, |
| kTrackedServerIdWithoutServerTagMatchesPermanentNode = 12, |
| kMaxValue = kTrackedServerIdWithoutServerTagMatchesPermanentNode, |
| }; |
| |
| syncer::UniquePosition RandomUniquePosition() { |
| return syncer::UniquePosition::InitialPosition( |
| syncer::UniquePosition::RandomSuffix()); |
| } |
| |
| // Returns a sync ID mimic-ing what a real server could return, which means it |
| // generally opaque for the client but deterministic given |guid|, because the |
| // sync ID is roughly a hashed GUID, at least in normal circumnstances where the |
| // GUID is used either as client tag hash or as originator client item ID. |
| std::string GetFakeServerIdFromGUID(const base::GUID& guid) { |
| // For convenience in tests, |guid| may refer to permanent nodes too, |
| // and yet the returned sync ID will honor the sync ID constants for permanent |
| // nodes. |
| if (guid.AsLowercaseString() == |
| bookmarks::BookmarkNode::kBookmarkBarNodeGuid) { |
| return kBookmarkBarId; |
| } |
| if (guid.AsLowercaseString() == |
| bookmarks::BookmarkNode::kOtherBookmarksNodeGuid) { |
| return kOtherBookmarksId; |
| } |
| if (guid.AsLowercaseString() == |
| bookmarks::BookmarkNode::kMobileBookmarksNodeGuid) { |
| return kMobileBookmarksId; |
| } |
| return base::StrCat({"server_id_for_", guid.AsLowercaseString()}); |
| } |
| |
| // |node| must not be nullptr. |
| sync_pb::BookmarkMetadata CreateNodeMetadata( |
| const bookmarks::BookmarkNode* node, |
| const syncer::UniquePosition& unique_position) { |
| DCHECK(node); |
| sync_pb::BookmarkMetadata bookmark_metadata; |
| bookmark_metadata.set_id(node->id()); |
| bookmark_metadata.mutable_metadata()->set_server_id( |
| GetFakeServerIdFromGUID(node->guid())); |
| bookmark_metadata.mutable_metadata()->set_client_tag_hash( |
| syncer::ClientTagHash::FromUnhashed(syncer::BOOKMARKS, |
| node->guid().AsLowercaseString()) |
| .value()); |
| *bookmark_metadata.mutable_metadata()->mutable_unique_position() = |
| unique_position.ToProto(); |
| return bookmark_metadata; |
| } |
| |
| // |node| must not be nullptr. |
| sync_pb::BookmarkMetadata CreatePermanentNodeMetadata( |
| const bookmarks::BookmarkNode* node, |
| const std::string& server_id) { |
| sync_pb::BookmarkMetadata bookmark_metadata; |
| bookmark_metadata.set_id(node->id()); |
| bookmark_metadata.mutable_metadata()->set_server_id(server_id); |
| return bookmark_metadata; |
| } |
| |
| sync_pb::BookmarkModelMetadata CreateMetadataForPermanentNodes( |
| const bookmarks::BookmarkModel* bookmark_model) { |
| sync_pb::BookmarkModelMetadata model_metadata; |
| model_metadata.mutable_model_type_state()->set_initial_sync_done(true); |
| model_metadata.set_bookmarks_hierarchy_fields_reuploaded(true); |
| |
| *model_metadata.add_bookmarks_metadata() = |
| CreatePermanentNodeMetadata(bookmark_model->bookmark_bar_node(), |
| /*server_id=*/kBookmarkBarId); |
| *model_metadata.add_bookmarks_metadata() = |
| CreatePermanentNodeMetadata(bookmark_model->mobile_node(), |
| /*server_id=*/kMobileBookmarksId); |
| *model_metadata.add_bookmarks_metadata() = |
| CreatePermanentNodeMetadata(bookmark_model->other_node(), |
| /*server_id=*/kOtherBookmarksId); |
| |
| return model_metadata; |
| } |
| |
| syncer::UpdateResponseData CreateTombstoneResponseData(const base::GUID& guid, |
| int version) { |
| syncer::EntityData data; |
| data.id = GetFakeServerIdFromGUID(guid); |
| |
| // EntityData is considered a deletion if its specifics hasn't been set. |
| DCHECK(data.is_deleted()); |
| |
| syncer::UpdateResponseData response_data; |
| response_data.entity = std::move(data); |
| response_data.response_version = version; |
| |
| return response_data; |
| } |
| |
| syncer::UpdateResponseData CreateUpdateResponseData( |
| const base::GUID& guid, |
| const base::GUID& parent_guid, |
| const std::string& title, |
| int version, |
| const syncer::UniquePosition& unique_position) { |
| syncer::UpdateResponseData response_data = |
| CreateTombstoneResponseData(guid, version); |
| |
| response_data.entity.originator_client_item_id = guid.AsLowercaseString(); |
| |
| sync_pb::BookmarkSpecifics* bookmark_specifics = |
| response_data.entity.specifics.mutable_bookmark(); |
| bookmark_specifics->set_guid(guid.AsLowercaseString()); |
| bookmark_specifics->set_parent_guid(parent_guid.AsLowercaseString()); |
| bookmark_specifics->set_legacy_canonicalized_title(title); |
| bookmark_specifics->set_full_title(title); |
| bookmark_specifics->set_type(sync_pb::BookmarkSpecifics::FOLDER); |
| *bookmark_specifics->mutable_unique_position() = unique_position.ToProto(); |
| |
| DCHECK(!response_data.entity.is_deleted()); |
| return response_data; |
| } |
| |
| // Overload that assign a random position. Should only be used when the title, |
| // version and position are irrelevant. |
| syncer::UpdateResponseData CreateUpdateResponseData( |
| const base::GUID& guid, |
| const base::GUID& parent_guid) { |
| return CreateUpdateResponseData( |
| guid, parent_guid, base::StrCat({"Title for ", guid.AsLowercaseString()}), |
| /*version=*/0, RandomUniquePosition()); |
| } |
| |
| syncer::UpdateResponseData CreateBookmarkRootUpdateData() { |
| syncer::EntityData data; |
| data.id = syncer::ModelTypeToRootTag(syncer::BOOKMARKS); |
| data.server_defined_unique_tag = |
| syncer::ModelTypeToRootTag(syncer::BOOKMARKS); |
| |
| data.specifics.mutable_bookmark(); |
| |
| syncer::UpdateResponseData response_data; |
| response_data.entity = std::move(data); |
| // Similar to what's done in the loopback_server. |
| response_data.response_version = 0; |
| return response_data; |
| } |
| |
| syncer::UpdateResponseData CreatePermanentFolderUpdateData( |
| const std::string& id, |
| const std::string& tag) { |
| syncer::EntityData data; |
| data.id = id; |
| data.server_defined_unique_tag = tag; |
| |
| data.specifics.mutable_bookmark(); |
| |
| syncer::UpdateResponseData response_data; |
| response_data.entity = std::move(data); |
| // Similar to what's done in the loopback_server. |
| response_data.response_version = 0; |
| return response_data; |
| } |
| |
| syncer::UpdateResponseDataList CreatePermanentFoldersUpdateData() { |
| syncer::UpdateResponseDataList updates; |
| updates.push_back( |
| CreatePermanentFolderUpdateData(kBookmarkBarId, kBookmarkBarTag)); |
| updates.push_back( |
| CreatePermanentFolderUpdateData(kOtherBookmarksId, kOtherBookmarksTag)); |
| updates.push_back( |
| CreatePermanentFolderUpdateData(kMobileBookmarksId, kMobileBookmarksTag)); |
| return updates; |
| } |
| |
| class BookmarkRemoteUpdatesHandlerWithInitialMergeTest : public testing::Test { |
| public: |
| BookmarkRemoteUpdatesHandlerWithInitialMergeTest() |
| : bookmark_model_(bookmarks::TestBookmarkClient::CreateModel()), |
| tracker_(SyncedBookmarkTracker::CreateEmpty(sync_pb::ModelTypeState())), |
| updates_handler_(bookmark_model_.get(), |
| &favicon_service_, |
| tracker_.get()) { |
| BookmarkModelMerger(CreatePermanentFoldersUpdateData(), |
| bookmark_model_.get(), &favicon_service_, |
| tracker_.get()) |
| .Merge(); |
| } |
| |
| bookmarks::BookmarkModel* bookmark_model() { return bookmark_model_.get(); } |
| SyncedBookmarkTracker* tracker() { return tracker_.get(); } |
| favicon::MockFaviconService* favicon_service() { return &favicon_service_; } |
| BookmarkRemoteUpdatesHandler* updates_handler() { return &updates_handler_; } |
| |
| const base::GUID kBookmarkBarGuid = |
| base::GUID::ParseLowercase(bookmarks::BookmarkNode::kBookmarkBarNodeGuid); |
| |
| private: |
| std::unique_ptr<bookmarks::BookmarkModel> bookmark_model_; |
| std::unique_ptr<SyncedBookmarkTracker> tracker_; |
| testing::NiceMock<favicon::MockFaviconService> favicon_service_; |
| BookmarkRemoteUpdatesHandler updates_handler_; |
| }; |
| |
| TEST(BookmarkRemoteUpdatesHandlerReorderUpdatesTest, ShouldIgnoreRootNode) { |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateBookmarkRootUpdateData()); |
| std::vector<const syncer::UpdateResponseData*> ordered_updates = |
| BookmarkRemoteUpdatesHandler::ReorderValidUpdatesForTest(&updates); |
| // Root node update should be filtered out. |
| EXPECT_THAT(ordered_updates.size(), Eq(0U)); |
| } |
| |
| TEST(BookmarkRemoteUpdatesHandlerReorderUpdatesTest, |
| ShouldIgnorePermanentNodes) { |
| syncer::UpdateResponseDataList updates = CreatePermanentFoldersUpdateData(); |
| std::vector<const syncer::UpdateResponseData*> ordered_updates = |
| BookmarkRemoteUpdatesHandler::ReorderValidUpdatesForTest(&updates); |
| // Root node update should be filtered out. |
| EXPECT_THAT(ordered_updates.size(), Eq(0U)); |
| } |
| |
| TEST(BookmarkRemoteUpdatesHandlerReorderUpdatesTest, |
| ShouldIgnoreInvalidSpecifics) { |
| const std::string kTitle = "title"; |
| const syncer::UniquePosition kPosition = RandomUniquePosition(); |
| const base::GUID kBookmarkBarGuid = |
| base::GUID::ParseLowercase(bookmarks::BookmarkNode::kBookmarkBarNodeGuid); |
| |
| syncer::UpdateResponseDataList updates; |
| |
| // Create update with an invalid GUID. |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/base::GUID(), |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/0, |
| /*unique_position=*/kPosition)); |
| |
| base::HistogramTester histogram_tester; |
| std::vector<const syncer::UpdateResponseData*> ordered_updates = |
| BookmarkRemoteUpdatesHandler::ReorderValidUpdatesForTest(&updates); |
| |
| // The update should be filtered out. |
| EXPECT_THAT(ordered_updates.size(), Eq(0U)); |
| |
| histogram_tester.ExpectBucketCount( |
| "Sync.ProblematicServerSideBookmarks", |
| /*sample=*/ExpectedRemoteBookmarkUpdateError::kInvalidSpecifics, |
| /*expected_count=*/1); |
| } |
| |
| TEST(BookmarkRemoteUpdatesHandlerReorderUpdatesTest, |
| ShouldReorderParentsUpdateBeforeChildrenAndBothBeforeDeletions) { |
| const base::GUID kBookmarkBarGuid = |
| base::GUID::ParseLowercase(bookmarks::BookmarkNode::kBookmarkBarNodeGuid); |
| |
| // Prepare creation updates to build this structure: |
| // bookmark_bar |
| // |- node0 |
| // |- node1 |
| // |- node2 |
| // and another sub hierarchy under node3 that won't receive any update. |
| // node4 |
| // |- node5 |
| // and a deletion for node6. |
| |
| // Constuct the updates list to have deletion first, and then all creations in |
| // reverse shuffled order (from child to parent). |
| |
| std::vector<base::GUID> guids; |
| for (int i = 0; i < 7; i++) { |
| // Use non-random GUIDs to produce a deterministic test outcome, since the |
| // precise sync IDs can change the final order in ways that don't matter. |
| guids.push_back(base::GUID::ParseLowercase( |
| base::StringPrintf("00000000-0000-4000-a000-00000000000%d", i))); |
| } |
| |
| // Construct updates list |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateTombstoneResponseData(/*guid=*/guids[6], |
| /*version=*/1)); |
| updates.push_back(CreateUpdateResponseData(/*guid=*/guids[5], |
| /*parent_guid=*/guids[4])); |
| updates.push_back(CreateUpdateResponseData(/*guid=*/guids[2], |
| /*parent_guid=*/guids[1])); |
| updates.push_back(CreateUpdateResponseData(/*guid=*/guids[1], |
| /*parent_guid=*/guids[0])); |
| updates.push_back(CreateUpdateResponseData(/*guid=*/guids[4], |
| /*parent_guid=*/guids[3])); |
| updates.push_back(CreateUpdateResponseData(/*guid=*/guids[0], |
| /*parent_guid=*/kBookmarkBarGuid)); |
| |
| std::vector<const syncer::UpdateResponseData*> ordered_updates = |
| BookmarkRemoteUpdatesHandler::ReorderValidUpdatesForTest(&updates); |
| |
| std::vector<std::string> ordered_update_sync_ids; |
| for (const syncer::UpdateResponseData* update : ordered_updates) { |
| ordered_update_sync_ids.push_back(update->entity.id); |
| } |
| |
| // Updates should be ordered such that parent node update comes first, and |
| // deletions come last. The ordering requirements are within substrees only, |
| // since it doesn't matter whether node1 comes before or after node4, so there |
| // are two acceptable outcomes: |
| // A) node0 --> node1 --> node2 --> node4 --> node5 --> node6 |
| // B) node4 --> node5 --> node0 --> node1 --> node2 --> node6 |
| EXPECT_THAT(ordered_update_sync_ids, |
| AnyOf(ElementsAre(GetFakeServerIdFromGUID(guids[0]), |
| GetFakeServerIdFromGUID(guids[1]), |
| GetFakeServerIdFromGUID(guids[2]), |
| GetFakeServerIdFromGUID(guids[4]), |
| GetFakeServerIdFromGUID(guids[5]), |
| GetFakeServerIdFromGUID(guids[6])), |
| ElementsAre(GetFakeServerIdFromGUID(guids[4]), |
| GetFakeServerIdFromGUID(guids[5]), |
| GetFakeServerIdFromGUID(guids[0]), |
| GetFakeServerIdFromGUID(guids[1]), |
| GetFakeServerIdFromGUID(guids[2]), |
| GetFakeServerIdFromGUID(guids[6])))); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldProcessRandomlyOrderedCreations) { |
| // Prepare creation updates to construct this structure: |
| // bookmark_bar |
| // |- node0 |
| // |- node1 |
| // |- node2 |
| |
| const base::GUID kGuid0 = base::GUID::GenerateRandomV4(); |
| const base::GUID kGuid1 = base::GUID::GenerateRandomV4(); |
| const base::GUID kGuid2 = base::GUID::GenerateRandomV4(); |
| |
| // Constuct the updates list to have creations randomly ordered. |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateUpdateResponseData(/*guid=*/kGuid2, |
| /*parent_guid=*/kGuid1)); |
| updates.push_back(CreateUpdateResponseData(/*guid=*/kGuid0, |
| /*parent_guid=*/kBookmarkBarGuid)); |
| updates.push_back(CreateUpdateResponseData(/*guid=*/kGuid1, |
| /*parent_guid=*/kGuid0)); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // All nodes should be tracked including the "bookmark bar", "other |
| // bookmarks" node and "mobile bookmarks". |
| EXPECT_THAT(tracker()->TrackedEntitiesCountForTest(), Eq(6U)); |
| |
| // All nodes should have been added to the model. |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| ASSERT_THAT(bookmark_bar_node->children().size(), Eq(1u)); |
| EXPECT_THAT(bookmark_bar_node->children().front()->guid(), Eq(kGuid0)); |
| ASSERT_THAT(bookmark_bar_node->children().front()->children().size(), Eq(1u)); |
| const bookmarks::BookmarkNode* grandchild = |
| bookmark_bar_node->children().front()->children().front().get(); |
| EXPECT_THAT(grandchild->guid(), Eq(kGuid1)); |
| ASSERT_THAT(grandchild->children().size(), Eq(1u)); |
| EXPECT_THAT(grandchild->children().front()->guid(), Eq(kGuid2)); |
| EXPECT_THAT(grandchild->children().front()->children().size(), Eq(0u)); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldLogFreshnessToUma) { |
| const base::GUID kGuid = base::GUID::GenerateRandomV4(); |
| |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateUpdateResponseData(/*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid)); |
| |
| base::HistogramTester histogram_tester; |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| histogram_tester.ExpectTotalCount( |
| "Sync.NonReflectionUpdateFreshnessPossiblySkewed2.BOOKMARK", 1); |
| |
| // Process the same update again, which should be ignored because the version |
| // hasn't increased. |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| histogram_tester.ExpectTotalCount( |
| "Sync.NonReflectionUpdateFreshnessPossiblySkewed2.BOOKMARK", 1); |
| |
| // Increase version and process again; should log freshness. |
| ++updates[0].response_version; |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| histogram_tester.ExpectTotalCount( |
| "Sync.NonReflectionUpdateFreshnessPossiblySkewed2.BOOKMARK", 2); |
| |
| // Process remote deletion; should log freshness. |
| updates[0] = |
| CreateTombstoneResponseData(/*guid=*/kGuid, |
| /*version=*/updates[0].response_version + 1); |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| histogram_tester.ExpectTotalCount( |
| "Sync.NonReflectionUpdateFreshnessPossiblySkewed2.BOOKMARK", 3); |
| |
| // Process another (redundant) deletion for the same entity; should not log. |
| ++updates[0].response_version; |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| histogram_tester.ExpectTotalCount( |
| "Sync.NonReflectionUpdateFreshnessPossiblySkewed2.BOOKMARK", 3); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldProcessRandomlyOrderedDeletions) { |
| // Prepare creation updates to construct this structure: |
| // bookmark_bar |
| // |- node0 |
| // |- node1 |
| // |- node2 |
| |
| const base::GUID kGuid0 = base::GUID::GenerateRandomV4(); |
| const base::GUID kGuid1 = base::GUID::GenerateRandomV4(); |
| const base::GUID kGuid2 = base::GUID::GenerateRandomV4(); |
| |
| // Construct the updates list to create that structure |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateUpdateResponseData(/*guid=*/kGuid0, |
| /*parent_guid=*/kBookmarkBarGuid)); |
| updates.push_back(CreateUpdateResponseData(/*guid=*/kGuid1, |
| /*parent_guid=*/kGuid0)); |
| updates.push_back(CreateUpdateResponseData(/*guid=*/kGuid2, |
| /*parent_guid=*/kGuid1)); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // All nodes should be tracked including the "bookmark bar", "other |
| // bookmarks" node and "mobile bookmarks". |
| ASSERT_THAT(tracker()->TrackedEntitiesCountForTest(), Eq(6U)); |
| |
| // Construct the updates list to have random deletions order. |
| updates.clear(); |
| updates.push_back(CreateTombstoneResponseData(/*guid=*/kGuid1, |
| /*version=*/1)); |
| updates.push_back(CreateTombstoneResponseData(/*guid=*/kGuid0, |
| /*version=*/1)); |
| updates.push_back(CreateTombstoneResponseData(/*guid=*/kGuid2, |
| /*version=*/1)); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // |tracker| should have only permanent nodes now. |
| EXPECT_THAT(tracker()->TrackedEntitiesCountForTest(), Eq(3U)); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldProcessDeletionWithServerIdOnly) { |
| const base::GUID kGuid0 = base::GUID::GenerateRandomV4(); |
| |
| // Construct the updates list to create that structure |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateUpdateResponseData(/*guid=*/kGuid0, |
| /*parent_guid=*/kBookmarkBarGuid)); |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // The node should be tracked including the "bookmark bar", "other |
| // bookmarks" node and "mobile bookmarks". |
| ASSERT_THAT(tracker()->TrackedEntitiesCountForTest(), Eq(4U)); |
| |
| // Construct the updates list with one minimalistic deletion (server ID only). |
| updates.clear(); |
| updates.emplace_back(); |
| updates.back().entity.id = GetFakeServerIdFromGUID(kGuid0); |
| updates.back().response_version = 1; |
| ASSERT_TRUE(updates.back().entity.is_deleted()); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // |tracker| should have only permanent nodes now. |
| EXPECT_THAT(tracker()->TrackedEntitiesCountForTest(), Eq(3U)); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldIgnoreRemoteCreationWithInvalidGuidInSpecifics) { |
| const std::string kTitle = "title"; |
| const syncer::UniquePosition kPosition = RandomUniquePosition(); |
| |
| syncer::UpdateResponseDataList updates; |
| |
| // Create update with an invalid GUID. |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/base::GUID(), |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/0, |
| /*unique_position=*/kPosition)); |
| |
| base::HistogramTester histogram_tester; |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| EXPECT_THAT(tracker()->GetEntityForSyncId(updates[0].entity.id), IsNull()); |
| |
| histogram_tester.ExpectBucketCount( |
| "Sync.ProblematicServerSideBookmarks", |
| /*sample=*/ExpectedRemoteBookmarkUpdateError::kInvalidSpecifics, |
| /*expected_count=*/1); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldIgnoreRemoteCreationWithUnexpectedGuidInSpecifics) { |
| const base::GUID kOriginalGuid = base::GUID::GenerateRandomV4(); |
| const base::GUID kGuidInSpecifics = base::GUID::GenerateRandomV4(); |
| const std::string kTitle = "title"; |
| const syncer::UniquePosition kPosition = RandomUniquePosition(); |
| |
| syncer::UpdateResponseDataList updates; |
| |
| // Create update with empty GUID. |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kOriginalGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/0, |
| /*unique_position=*/kPosition)); |
| |
| // Override the GUID in specifics to mimic a mismatch with respecto to the |
| // client tag hash. |
| updates.back().entity.specifics.mutable_bookmark()->set_guid( |
| kGuidInSpecifics.AsLowercaseString()); |
| |
| base::HistogramTester histogram_tester; |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| EXPECT_THAT(tracker()->GetEntityForGUID(kOriginalGuid), IsNull()); |
| EXPECT_THAT(tracker()->GetEntityForGUID(kGuidInSpecifics), IsNull()); |
| |
| histogram_tester.ExpectBucketCount( |
| "Sync.ProblematicServerSideBookmarks", |
| /*sample=*/ExpectedRemoteBookmarkUpdateError::kUnexpectedGuid, |
| /*expected_count=*/1); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldIgnoreMisbehavingServerWithRemoteGuidUpdate) { |
| const std::string kTitle = "title"; |
| const base::GUID kOldGuid = base::GUID::GenerateRandomV4(); |
| const base::GUID kNewGuid = base::GUID::GenerateRandomV4(); |
| const syncer::UniquePosition kPosition = RandomUniquePosition(); |
| |
| syncer::UpdateResponseDataList updates; |
| |
| // Create update with GUID. |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kOldGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/0, |
| /*unique_position=*/kPosition)); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| ASSERT_THAT(tracker()->GetEntityForGUID(kOldGuid), NotNull()); |
| ASSERT_THAT(tracker()->GetEntityForGUID(kNewGuid), IsNull()); |
| |
| // Push an update for the same entity with a new GUID. Note that this is a |
| // protocol violation, because |originator_client_item_id| cannot have changed |
| // for a given server ID. |
| updates.clear(); |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kNewGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/1, |
| /*unique_position=*/RandomUniquePosition())); |
| updates[0].entity.id = GetFakeServerIdFromGUID(kOldGuid); |
| |
| base::HistogramTester histogram_tester; |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| EXPECT_THAT(tracker()->GetEntityForGUID(kOldGuid), NotNull()); |
| EXPECT_THAT(tracker()->GetEntityForGUID(kNewGuid), IsNull()); |
| |
| // The GUID should not have been updated. |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| ASSERT_THAT(bookmark_bar_node->children().size(), Eq(1u)); |
| EXPECT_THAT(bookmark_bar_node->children().front()->guid(), Eq(kOldGuid)); |
| |
| histogram_tester.ExpectBucketCount( |
| "Sync.ProblematicServerSideBookmarks", |
| /*sample=*/ |
| ExpectedRemoteBookmarkUpdateError::kGuidChangedForTrackedServerId, |
| /*expected_count=*/1); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldIgnoreMisbehavingServerWithPermanentNodeUpdateWithoutServerTag) { |
| ASSERT_THAT(tracker()->GetEntityForSyncId(kBookmarkBarId), NotNull()); |
| |
| // Push an update for a permanent entity, but without a unique server tag. |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/bookmark_model()->bookmark_bar_node()->guid(), |
| /*parent_guid=*/base::GUID::GenerateRandomV4(), |
| /*title=*/"title", |
| /*version=*/1, |
| /*unique_position=*/RandomUniquePosition())); |
| |
| base::HistogramTester histogram_tester; |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| histogram_tester.ExpectBucketCount( |
| "Sync.ProblematicServerSideBookmarks", |
| /*sample=*/ |
| ExpectedRemoteBookmarkUpdateError:: |
| kTrackedServerIdWithoutServerTagMatchesPermanentNode, |
| /*expected_count=*/1); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldPositionRemoteCreations) { |
| // Prepare creation updates to construct this structure: |
| // bookmark_bar |
| // |- node0 |
| // |- node1 |
| // |- node2 |
| |
| const base::GUID kGuid0 = base::GUID::GenerateRandomV4(); |
| const base::GUID kGuid1 = base::GUID::GenerateRandomV4(); |
| const base::GUID kGuid2 = base::GUID::GenerateRandomV4(); |
| |
| const std::string kTitle0 = "title 0"; |
| const std::string kTitle1 = "title 1"; |
| const std::string kTitle2 = "title 2"; |
| |
| syncer::UniquePosition pos0 = syncer::UniquePosition::InitialPosition( |
| syncer::UniquePosition::RandomSuffix()); |
| syncer::UniquePosition pos1 = syncer::UniquePosition::After( |
| pos0, syncer::UniquePosition::RandomSuffix()); |
| syncer::UniquePosition pos2 = syncer::UniquePosition::After( |
| pos1, syncer::UniquePosition::RandomSuffix()); |
| |
| // Constuct the updates list to have creations randomly ordered. |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateUpdateResponseData(/*guid=*/kGuid2, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle2, |
| /*version=*/0, |
| /*unique_position=*/pos2)); |
| updates.push_back(CreateUpdateResponseData(/*guid=*/kGuid0, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle0, |
| /*version=*/0, |
| /*unique_position=*/pos0)); |
| updates.push_back(CreateUpdateResponseData(/*guid=*/kGuid1, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle1, |
| /*version=*/0, |
| /*unique_position=*/pos1)); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // All nodes should have been added to the model in the correct order. |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| ASSERT_THAT(bookmark_bar_node->children().size(), Eq(3u)); |
| EXPECT_THAT(bookmark_bar_node->children()[0]->guid(), Eq(kGuid0)); |
| EXPECT_THAT(bookmark_bar_node->children()[1]->guid(), Eq(kGuid1)); |
| EXPECT_THAT(bookmark_bar_node->children()[2]->guid(), Eq(kGuid2)); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldPositionRemoteMovesToTheLeft) { |
| // Start with structure: |
| // bookmark_bar |
| // |- node0 |
| // |- node1 |
| // |- node2 |
| // |- node3 |
| // |- node4 |
| |
| std::vector<std::string> ids; |
| std::vector<base::GUID> guids; |
| std::vector<syncer::UniquePosition> positions; |
| |
| syncer::UniquePosition position = syncer::UniquePosition::InitialPosition( |
| syncer::UniquePosition::RandomSuffix()); |
| syncer::UpdateResponseDataList updates; |
| for (int i = 0; i < 5; i++) { |
| ids.push_back("node" + base::NumberToString(i)); |
| guids.push_back(base::GUID::GenerateRandomV4()); |
| position = syncer::UniquePosition::After( |
| position, syncer::UniquePosition::RandomSuffix()); |
| positions.push_back(position); |
| updates.push_back( |
| CreateUpdateResponseData(/*guid=*/guids[i], |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/"Title", |
| /*version=*/0, |
| /*unique_position=*/positions[i])); |
| } |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| EXPECT_THAT(bookmark_bar_node->children().size(), Eq(5u)); |
| |
| // Change it to this structure by moving node3 after node1. |
| // bookmark_bar |
| // |- node0 |
| // |- node1 |
| // |- node3 |
| // |- node2 |
| // |- node4 |
| |
| updates.clear(); |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/guids[3], |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/"Title", |
| /*version=*/1, |
| /*unique_position=*/ |
| syncer::UniquePosition::Between(positions[1], positions[2], |
| syncer::UniquePosition::RandomSuffix()))); |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // Model should have been updated. |
| ASSERT_THAT(bookmark_bar_node->children().size(), Eq(5u)); |
| EXPECT_THAT(bookmark_bar_node->children()[2]->guid(), Eq(guids[3])); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldPositionRemoteMovesToTheRight) { |
| // Start with structure: |
| // bookmark_bar |
| // |- node0 |
| // |- node1 |
| // |- node2 |
| // |- node3 |
| // |- node4 |
| |
| std::vector<std::string> ids; |
| std::vector<base::GUID> guids; |
| std::vector<syncer::UniquePosition> positions; |
| |
| syncer::UniquePosition position = syncer::UniquePosition::InitialPosition( |
| syncer::UniquePosition::RandomSuffix()); |
| syncer::UpdateResponseDataList updates; |
| for (int i = 0; i < 5; i++) { |
| ids.push_back("node" + base::NumberToString(i)); |
| guids.push_back(base::GUID::GenerateRandomV4()); |
| position = syncer::UniquePosition::After( |
| position, syncer::UniquePosition::RandomSuffix()); |
| positions.push_back(position); |
| updates.push_back( |
| CreateUpdateResponseData(/*guid=*/guids[i], |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/ids[i], |
| /*version=*/0, |
| /*unique_position=*/positions[i])); |
| } |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| EXPECT_THAT(bookmark_bar_node->children().size(), Eq(5u)); |
| |
| // Change it to this structure by moving node1 after node3. |
| // bookmark_bar |
| // |- node0 |
| // |- node2 |
| // |- node3 |
| // |- node1 |
| // |- node4 |
| |
| updates.clear(); |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/guids[1], |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/ids[1], |
| /*version=*/1, |
| /*unique_position=*/ |
| syncer::UniquePosition::Between(positions[3], positions[4], |
| syncer::UniquePosition::RandomSuffix()))); |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // Model should have been updated. |
| ASSERT_THAT(bookmark_bar_node->children().size(), Eq(5u)); |
| EXPECT_THAT(bookmark_bar_node->children()[3]->guid(), Eq(guids[1])); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldPositionRemoteReparenting) { |
| // Start with structure: |
| // bookmark_bar |
| // |- node0 |
| // |- node1 |
| // |- node2 |
| // |- node3 |
| // |- node4 |
| |
| std::vector<std::string> ids; |
| std::vector<base::GUID> guids; |
| std::vector<syncer::UniquePosition> positions; |
| |
| syncer::UniquePosition position = syncer::UniquePosition::InitialPosition( |
| syncer::UniquePosition::RandomSuffix()); |
| syncer::UpdateResponseDataList updates; |
| for (int i = 0; i < 5; i++) { |
| ids.push_back("node" + base::NumberToString(i)); |
| guids.push_back(base::GUID::GenerateRandomV4()); |
| position = syncer::UniquePosition::After( |
| position, syncer::UniquePosition::RandomSuffix()); |
| positions.push_back(position); |
| updates.push_back( |
| CreateUpdateResponseData(/*guid=*/guids[i], |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/"Title", |
| /*version=*/0, |
| /*unique_position=*/positions[i])); |
| } |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| EXPECT_THAT(bookmark_bar_node->children().size(), Eq(5u)); |
| |
| // Change it to this structure by moving node4 under node1. |
| // bookmark_bar |
| // |- node0 |
| // |- node1 |
| // |- node4 |
| // |- node2 |
| // |- node3 |
| |
| updates.clear(); |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/guids[4], |
| /*parent_guid=*/guids[1], |
| /*title=*/"Title", |
| /*version=*/1, |
| /*unique_position=*/RandomUniquePosition())); |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // Model should have been updated. |
| ASSERT_THAT(bookmark_bar_node->children().size(), Eq(4u)); |
| ASSERT_THAT(bookmark_bar_node->children()[1]->children().size(), Eq(1u)); |
| EXPECT_THAT(bookmark_bar_node->children()[1]->children()[0]->guid(), |
| Eq(guids[4])); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldIgnoreNodeIfMissingParentNode) { |
| // Prepare creation updates to construct this structure: |
| // bookmark_bar |
| |
| const base::GUID kMissingParentGuid = base::GUID::GenerateRandomV4(); |
| const base::GUID kChildGuid = base::GUID::GenerateRandomV4(); |
| const std::string kChildId = "child_id"; |
| const std::string kTitle = "Title"; |
| const GURL kUrl("http://www.url.com"); |
| |
| syncer::UpdateResponseDataList updates; |
| updates.push_back( |
| CreateUpdateResponseData(/*guid=*/kChildGuid, |
| /*parent_guid=*/kMissingParentGuid)); |
| |
| base::HistogramTester histogram_tester; |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| EXPECT_EQ(bookmark_bar_node->children().size(), 0U); |
| histogram_tester.ExpectBucketCount( |
| "Sync.ProblematicServerSideBookmarks", |
| /*sample=*/ExpectedRemoteBookmarkUpdateError::kMissingParentNode, |
| /*expected_count=*/1); |
| |
| EXPECT_THAT(tracker()->GetNumIgnoredUpdatesDueToMissingParentForTest(), |
| Eq(1)); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldIgnoreNodeIfParentIsNotFolder) { |
| // Prepare creation updates to construct this structure: |
| // bookmark_bar |
| // |- node0 (is_folder=false) |
| // |- node1 |
| |
| const base::GUID kParentGuid = base::GUID::GenerateRandomV4(); |
| const base::GUID kChildGuid = base::GUID::GenerateRandomV4(); |
| const std::string kTitle = "Title"; |
| const GURL kUrl("http://www.url.com"); |
| |
| syncer::UpdateResponseDataList updates; |
| syncer::EntityData data; |
| data.id = GetFakeServerIdFromGUID(kParentGuid); |
| sync_pb::BookmarkSpecifics* bookmark_specifics = |
| data.specifics.mutable_bookmark(); |
| bookmark_specifics->set_guid(kParentGuid.AsLowercaseString()); |
| bookmark_specifics->set_parent_guid( |
| bookmarks::BookmarkNode::kBookmarkBarNodeGuid); |
| bookmark_specifics->set_legacy_canonicalized_title(kTitle); |
| bookmark_specifics->set_url(kUrl.spec()); |
| bookmark_specifics->set_type(sync_pb::BookmarkSpecifics::URL); |
| *bookmark_specifics->mutable_unique_position() = |
| RandomUniquePosition().ToProto(); |
| data.originator_client_item_id = bookmark_specifics->guid(); |
| |
| ASSERT_TRUE(IsValidBookmarkSpecifics(*bookmark_specifics)); |
| |
| syncer::UpdateResponseData response_data; |
| response_data.entity = std::move(data); |
| // Similar to what's done in the loopback_server. |
| response_data.response_version = 0; |
| |
| updates.push_back(std::move(response_data)); |
| |
| updates.push_back(CreateUpdateResponseData(/*guid=*/kChildGuid, |
| /*parent_guid=*/kParentGuid)); |
| |
| base::HistogramTester histogram_tester; |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| ASSERT_EQ(bookmark_bar_node->children().size(), 1U); |
| EXPECT_TRUE(bookmark_bar_node->children()[0]->children().empty()); |
| histogram_tester.ExpectBucketCount( |
| "Sync.ProblematicServerSideBookmarks", |
| /*sample=*/ExpectedRemoteBookmarkUpdateError::kParentNotFolder, |
| /*expected_count=*/1); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldMergeFaviconUponRemoteCreationsWithFavicon) { |
| // Prepare creation updates to construct this structure: |
| // bookmark_bar |
| // |- node0 |
| |
| const std::string kTitle = "Title"; |
| const GURL kUrl("http://www.url.com"); |
| const GURL kIconUrl("http://www.icon-url.com"); |
| |
| syncer::UpdateResponseDataList updates; |
| syncer::EntityData data; |
| data.id = "server_id"; |
| sync_pb::BookmarkSpecifics* bookmark_specifics = |
| data.specifics.mutable_bookmark(); |
| bookmark_specifics->set_guid( |
| base::GUID::GenerateRandomV4().AsLowercaseString()); |
| bookmark_specifics->set_parent_guid( |
| bookmarks::BookmarkNode::kBookmarkBarNodeGuid); |
| // Use the server id as the title for simplicity. |
| bookmark_specifics->set_legacy_canonicalized_title(kTitle); |
| bookmark_specifics->set_url(kUrl.spec()); |
| bookmark_specifics->set_icon_url(kIconUrl.spec()); |
| bookmark_specifics->set_favicon("PNG"); |
| bookmark_specifics->set_type(sync_pb::BookmarkSpecifics::URL); |
| *bookmark_specifics->mutable_unique_position() = |
| RandomUniquePosition().ToProto(); |
| data.originator_client_item_id = bookmark_specifics->guid(); |
| |
| ASSERT_TRUE(IsValidBookmarkSpecifics(*bookmark_specifics)); |
| |
| syncer::UpdateResponseData response_data; |
| response_data.entity = std::move(data); |
| // Similar to what's done in the loopback_server. |
| response_data.response_version = 0; |
| |
| updates.push_back(std::move(response_data)); |
| |
| EXPECT_CALL(*favicon_service(), |
| AddPageNoVisitForBookmark(kUrl, base::UTF8ToUTF16(kTitle))); |
| EXPECT_CALL(*favicon_service(), MergeFavicon(kUrl, kIconUrl, _, _, _)); |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldDeleteFaviconUponRemoteCreationsWithoutFavicon) { |
| // Prepare creation updates to construct this structure: |
| // bookmark_bar |
| // |- node0 |
| |
| const std::string kTitle = "Title"; |
| const GURL kUrl("http://www.url.com"); |
| |
| syncer::UpdateResponseDataList updates; |
| syncer::EntityData data; |
| data.id = "server_id"; |
| sync_pb::BookmarkSpecifics* bookmark_specifics = |
| data.specifics.mutable_bookmark(); |
| bookmark_specifics->set_guid( |
| base::GUID::GenerateRandomV4().AsLowercaseString()); |
| bookmark_specifics->set_parent_guid( |
| bookmarks::BookmarkNode::kBookmarkBarNodeGuid); |
| // Use the server id as the title for simplicity. |
| bookmark_specifics->set_legacy_canonicalized_title(kTitle); |
| bookmark_specifics->set_url(kUrl.spec()); |
| bookmark_specifics->set_type(sync_pb::BookmarkSpecifics::URL); |
| *bookmark_specifics->mutable_unique_position() = |
| RandomUniquePosition().ToProto(); |
| data.originator_client_item_id = bookmark_specifics->guid(); |
| |
| ASSERT_TRUE(IsValidBookmarkSpecifics(*bookmark_specifics)); |
| |
| syncer::UpdateResponseData response_data; |
| response_data.entity = std::move(data); |
| // Similar to what's done in the loopback_server. |
| response_data.response_version = 0; |
| |
| updates.push_back(std::move(response_data)); |
| |
| EXPECT_CALL(*favicon_service(), |
| DeleteFaviconMappings(ElementsAre(kUrl), |
| favicon_base::IconType::kFavicon)); |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| } |
| |
| // This tests the case when a local creation is successfully committed to the |
| // server but the commit respone isn't received for some reason. Further updates |
| // to that entity should update the sync id in the tracker. |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldUpdateSyncIdWhenRecevingUpdateForNewlyCreatedLocalNode) { |
| const std::string kCacheGuid = "generated_id"; |
| const base::GUID kBookmarkGuid = base::GUID::GenerateRandomV4(); |
| const std::string kOriginatorClientItemId = kBookmarkGuid.AsLowercaseString(); |
| const std::string kSyncId = "server_id"; |
| const int64_t kServerVersion = 1000; |
| const base::Time kModificationTime(base::Time::Now() - base::Seconds(1)); |
| |
| sync_pb::ModelTypeState model_type_state; |
| model_type_state.set_initial_sync_done(true); |
| |
| sync_pb::EntitySpecifics specifics; |
| sync_pb::BookmarkSpecifics* bookmark_specifics = specifics.mutable_bookmark(); |
| bookmark_specifics->set_guid( |
| base::GUID::GenerateRandomV4().AsLowercaseString()); |
| bookmark_specifics->set_parent_guid( |
| bookmarks::BookmarkNode::kBookmarkBarNodeGuid); |
| bookmark_specifics->set_legacy_canonicalized_title("Title"); |
| bookmark_specifics->set_type(sync_pb::BookmarkSpecifics::FOLDER); |
| *bookmark_specifics->mutable_unique_position() = |
| RandomUniquePosition().ToProto(); |
| |
| ASSERT_TRUE(IsValidBookmarkSpecifics(*bookmark_specifics)); |
| |
| bookmarks::BookmarkNode parent(/*id=*/1, base::GUID::GenerateRandomV4(), |
| GURL()); |
| bookmarks::BookmarkNode* node = |
| parent.Add(std::make_unique<bookmarks::BookmarkNode>( |
| /*id=*/2, kBookmarkGuid, GURL()), |
| /*index=*/0); |
| // Track a sync entity (similar to what happens after a local creation). The |
| // |originator_client_item_id| is used a temp sync id and mark the entity that |
| // it needs to be committed.. |
| const SyncedBookmarkTrackerEntity* entity = |
| tracker()->Add(node, /*sync_id=*/kOriginatorClientItemId, |
| /*server_version=*/0, kModificationTime, specifics); |
| tracker()->IncrementSequenceNumber(entity); |
| |
| ASSERT_THAT(tracker()->GetEntityForSyncId(kOriginatorClientItemId), |
| Eq(entity)); |
| |
| // Now receive an update with the actual server id. |
| syncer::UpdateResponseDataList updates; |
| syncer::EntityData data; |
| data.id = kSyncId; |
| data.originator_client_item_id = kOriginatorClientItemId; |
| // Set the other required fields. |
| data.specifics = specifics; |
| data.specifics.mutable_bookmark()->set_guid(kOriginatorClientItemId); |
| bookmark_specifics->set_type(sync_pb::BookmarkSpecifics::FOLDER); |
| *bookmark_specifics->mutable_unique_position() = |
| RandomUniquePosition().ToProto(); |
| |
| syncer::UpdateResponseData response_data; |
| response_data.entity = std::move(data); |
| // Similar to what's done in the loopback_server. |
| response_data.response_version = kServerVersion; |
| updates.push_back(std::move(response_data)); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // The sync id in the tracker should have been updated. |
| EXPECT_THAT(tracker()->GetEntityForSyncId(kOriginatorClientItemId), IsNull()); |
| EXPECT_THAT(tracker()->GetEntityForSyncId(kSyncId), Eq(entity)); |
| EXPECT_THAT(entity->metadata()->server_id(), Eq(kSyncId)); |
| EXPECT_THAT(entity->bookmark_node(), Eq(node)); |
| } |
| |
| // Same as above for bookmarks created with client tags. |
| TEST_F( |
| BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldUpdateSyncIdWhenRecevingUpdateForNewlyCreatedLocalNodeWithClientTag) { |
| const base::GUID kBookmarkGuid = base::GUID::GenerateRandomV4(); |
| const std::string kSyncId = "server_id"; |
| const int64_t kServerVersion = 1000; |
| const base::Time kModificationTime(base::Time::Now() - base::Seconds(1)); |
| |
| bookmarks::BookmarkNode parent(/*id=*/1, base::GUID::GenerateRandomV4(), |
| GURL()); |
| |
| sync_pb::ModelTypeState model_type_state; |
| model_type_state.set_initial_sync_done(true); |
| |
| sync_pb::EntitySpecifics specifics; |
| sync_pb::BookmarkSpecifics* bookmark_specifics = specifics.mutable_bookmark(); |
| bookmark_specifics->set_guid(kBookmarkGuid.AsLowercaseString()); |
| bookmark_specifics->set_parent_guid(parent.guid().AsLowercaseString()); |
| bookmark_specifics->set_legacy_canonicalized_title("Title"); |
| bookmark_specifics->set_type(sync_pb::BookmarkSpecifics::FOLDER); |
| *bookmark_specifics->mutable_unique_position() = |
| RandomUniquePosition().ToProto(); |
| |
| ASSERT_TRUE(IsValidBookmarkSpecifics(*bookmark_specifics)); |
| |
| bookmarks::BookmarkNode* node = |
| parent.Add(std::make_unique<bookmarks::BookmarkNode>( |
| /*id=*/2, kBookmarkGuid, GURL()), |
| /*index=*/0); |
| // Track a sync entity (similar to what happens after a local creation). |
| const SyncedBookmarkTrackerEntity* entity = |
| tracker()->Add(node, /*sync_id=*/kSyncId, /*server_version=*/0, |
| kModificationTime, specifics); |
| tracker()->IncrementSequenceNumber(entity); |
| |
| ASSERT_THAT(tracker()->GetEntityForSyncId(kSyncId), Eq(entity)); |
| |
| // Now receive an update with the actual server id. |
| syncer::UpdateResponseDataList updates; |
| syncer::EntityData data; |
| data.id = kSyncId; |
| data.client_tag_hash = syncer::ClientTagHash::FromUnhashed( |
| syncer::BOOKMARKS, kBookmarkGuid.AsLowercaseString()); |
| // Set the other required fields. |
| data.specifics = specifics; |
| data.specifics.mutable_bookmark()->set_guid( |
| kBookmarkGuid.AsLowercaseString()); |
| bookmark_specifics->set_type(sync_pb::BookmarkSpecifics::FOLDER); |
| *bookmark_specifics->mutable_unique_position() = |
| RandomUniquePosition().ToProto(); |
| ASSERT_TRUE(IsValidBookmarkSpecifics(data.specifics.bookmark())); |
| |
| syncer::UpdateResponseData response_data; |
| response_data.entity = std::move(data); |
| // Similar to what's done in the loopback_server. |
| response_data.response_version = kServerVersion; |
| updates.push_back(std::move(response_data)); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // The sync id in the tracker should have been updated. |
| EXPECT_THAT(tracker()->GetEntityForSyncId(kBookmarkGuid.AsLowercaseString()), |
| IsNull()); |
| EXPECT_THAT(tracker()->GetEntityForSyncId(kSyncId), Eq(entity)); |
| EXPECT_THAT(entity->metadata()->server_id(), Eq(kSyncId)); |
| EXPECT_THAT(entity->bookmark_node(), Eq(node)); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldRecommitWhenEncryptionIsOutOfDate) { |
| const base::GUID kGuid = base::GUID::GenerateRandomV4(); |
| |
| sync_pb::ModelTypeState model_type_state; |
| model_type_state.set_encryption_key_name("encryption_key_name"); |
| tracker()->set_model_type_state(model_type_state); |
| |
| syncer::UpdateResponseDataList updates; |
| syncer::UpdateResponseData response_data = |
| CreateUpdateResponseData(/*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid); |
| response_data.encryption_key_name = "out_of_date_encryption_key_name"; |
| updates.push_back(std::move(response_data)); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| ASSERT_THAT(tracker()->GetEntityForGUID(kGuid), NotNull()); |
| EXPECT_THAT(tracker()->GetEntityForGUID(kGuid)->IsUnsynced(), Eq(true)); |
| } |
| |
| // Tests that recommit will be initiated in case when there is a local tombstone |
| // and server's update has out of date encryption. |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldRecommitWhenEncryptionIsOutOfDateOnConflict) { |
| const base::GUID kGuid = base::GUID::GenerateRandomV4(); |
| |
| sync_pb::ModelTypeState model_type_state; |
| model_type_state.set_encryption_key_name("encryption_key_name"); |
| tracker()->set_model_type_state(model_type_state); |
| |
| // Create a new node and remove it locally. |
| syncer::UpdateResponseDataList updates; |
| syncer::UpdateResponseData response_data = |
| CreateUpdateResponseData(/*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/"title", |
| /*version=*/0, |
| /*unique_position=*/RandomUniquePosition()); |
| response_data.encryption_key_name = "encryption_key_name"; |
| updates.push_back(std::move(response_data)); |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| const SyncedBookmarkTrackerEntity* entity = |
| tracker()->GetEntityForGUID(kGuid); |
| ASSERT_THAT(entity, NotNull()); |
| ASSERT_THAT(entity->bookmark_node(), NotNull()); |
| ASSERT_THAT(entity->bookmark_node()->guid(), Eq(kGuid)); |
| |
| auto* node = entity->bookmark_node(); |
| tracker()->MarkDeleted(entity); |
| tracker()->IncrementSequenceNumber(entity); |
| bookmark_model()->Remove(node); |
| |
| // Process an update with outdated encryption. This should cause a conflict |
| // and the remote version must be applied. Local tombstone entity will be |
| // removed during processing conflict. |
| updates.clear(); |
| response_data = |
| CreateUpdateResponseData(/*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/"title", |
| /*version=*/1, |
| /*unique_position=*/RandomUniquePosition()); |
| response_data.encryption_key_name = "out_of_date_encryption_key_name"; |
| updates.push_back(std::move(response_data)); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| // |entity| may be deleted here while processing update during conflict |
| // resolution. |
| entity = tracker()->GetEntityForGUID(kGuid); |
| ASSERT_THAT(entity, NotNull()); |
| EXPECT_THAT(entity->IsUnsynced(), Eq(true)); |
| EXPECT_THAT(entity->bookmark_node(), NotNull()); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldRecommitWhenGotNewEncryptionRequirements) { |
| const base::GUID kGuid = base::GUID::GenerateRandomV4(); |
| |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateUpdateResponseData(/*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid)); |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| ASSERT_THAT(tracker()->GetEntityForGUID(kGuid), NotNull()); |
| EXPECT_THAT(tracker()->GetEntityForGUID(kGuid)->IsUnsynced(), Eq(false)); |
| |
| updates_handler()->Process(syncer::UpdateResponseDataList(), |
| /*got_new_encryption_requirements=*/true); |
| EXPECT_THAT(tracker()->GetEntityForGUID(kGuid)->IsUnsynced(), Eq(true)); |
| // Permanent nodes shouldn't be committed. They are only created on the server |
| // and synced down. |
| EXPECT_THAT(tracker()->GetEntityForSyncId(kBookmarkBarId)->IsUnsynced(), |
| Eq(false)); |
| } |
| |
| TEST_F( |
| BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldNotRecommitWhenEncryptionKeyNameMistmatchWithConflictWithDeletions) { |
| sync_pb::ModelTypeState model_type_state; |
| model_type_state.set_encryption_key_name("encryption_key_name"); |
| tracker()->set_model_type_state(model_type_state); |
| |
| // Create the bookmark with same encryption key name. |
| const std::string kTitle = "title"; |
| const base::GUID kGuid = base::GUID::GenerateRandomV4(); |
| |
| syncer::UpdateResponseDataList updates; |
| syncer::UpdateResponseData response_data = |
| CreateUpdateResponseData(/*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid); |
| response_data.encryption_key_name = "encryption_key_name"; |
| updates.push_back(std::move(response_data)); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| // The bookmark has been added and tracked. |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| ASSERT_THAT(bookmark_bar_node->children().size(), Eq(1u)); |
| const SyncedBookmarkTrackerEntity* entity = |
| tracker()->GetEntityForGUID(kGuid); |
| ASSERT_THAT(entity, NotNull()); |
| |
| // Mark the entity as deleted locally. |
| tracker()->MarkDeleted(entity); |
| tracker()->IncrementSequenceNumber(entity); |
| ASSERT_THAT(tracker()->GetEntityForGUID(kGuid)->IsUnsynced(), Eq(true)); |
| |
| // Remove the bookmark from the local bookmark model. |
| bookmark_model()->Remove(bookmark_bar_node->children().front().get()); |
| ASSERT_THAT(bookmark_bar_node->children().size(), Eq(0u)); |
| |
| // Push a remote deletion for the same entity with an out of date encryption |
| // key name. |
| updates.clear(); |
| syncer::UpdateResponseData response_data2 = |
| CreateTombstoneResponseData(/*guid=*/kGuid, /*version=*/1); |
| response_data2.encryption_key_name = "out_of_date_encryption_key_name"; |
| // Increment the server version to make sure the update isn't discarded as |
| // reflection. |
| response_data2.response_version++; |
| updates.push_back(std::move(response_data2)); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // There should have been conflict, and it should have been resolved by |
| // removing local entity since both changes are deletions. |
| EXPECT_THAT(tracker()->GetEntityForGUID(kGuid), IsNull()); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldNotRecommitUptoDateEntitiesWhenGotNewEncryptionRequirements) { |
| const base::GUID kGuid0 = base::GUID::GenerateRandomV4(); |
| |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateUpdateResponseData(/*guid=*/kGuid0, |
| /*parent_guid=*/kBookmarkBarGuid)); |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| ASSERT_THAT(tracker()->GetEntityForGUID(kGuid0), NotNull()); |
| EXPECT_THAT(tracker()->GetEntityForGUID(kGuid0)->IsUnsynced(), Eq(false)); |
| |
| // Push another update to for the same entity. |
| syncer::UpdateResponseData response_data = |
| CreateUpdateResponseData(/*guid=*/kGuid0, |
| /*parent_guid=*/kBookmarkBarGuid); |
| |
| // Increment the server version to make sure the update isn't discarded as |
| // reflection. |
| response_data.response_version++; |
| syncer::UpdateResponseDataList new_updates; |
| new_updates.push_back(std::move(response_data)); |
| updates_handler()->Process(new_updates, |
| /*got_new_encryption_requirements=*/true); |
| EXPECT_THAT(tracker()->GetEntityForGUID(kGuid0)->IsUnsynced(), Eq(false)); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldResolveConflictBetweenLocalAndRemoteDeletionsByMatchingThem) { |
| const std::string kTitle = "title"; |
| const base::GUID kGuid = base::GUID::GenerateRandomV4(); |
| |
| syncer::UpdateResponseDataList updates; |
| |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/0, |
| /*unique_position=*/RandomUniquePosition())); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| const SyncedBookmarkTrackerEntity* entity = |
| tracker()->GetEntityForGUID(kGuid); |
| ASSERT_THAT(entity, NotNull()); |
| ASSERT_THAT(entity->IsUnsynced(), Eq(false)); |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| ASSERT_THAT(bookmark_bar_node->children().size(), Eq(1u)); |
| |
| // Mark the entity as deleted locally. |
| tracker()->MarkDeleted(entity); |
| tracker()->IncrementSequenceNumber(entity); |
| ASSERT_THAT(entity->IsUnsynced(), Eq(true)); |
| |
| // Remove the bookmark from the local bookmark model. |
| bookmark_model()->Remove(bookmark_bar_node->children().front().get()); |
| ASSERT_THAT(bookmark_bar_node->children().size(), Eq(0u)); |
| |
| // Push a remote deletion for the same entity. |
| updates.clear(); |
| updates.push_back(CreateTombstoneResponseData( |
| /*guid=*/kGuid, |
| /*version=*/1)); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // There should have been conflict, and it should have been resolved by |
| // removing local entity since both changes are deletions. |
| EXPECT_THAT(tracker()->GetEntityForGUID(kGuid), IsNull()); |
| // Make sure the bookmark hasn't been resurrected. |
| EXPECT_THAT(bookmark_bar_node->children().size(), Eq(0u)); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldResolveConflictBetweenLocalUpdateAndRemoteDeletionWithLocal) { |
| const std::string kTitle = "title"; |
| const base::GUID kGuid = base::GUID::GenerateRandomV4(); |
| |
| syncer::UpdateResponseDataList updates; |
| |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/0, |
| /*unique_position=*/RandomUniquePosition())); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| const SyncedBookmarkTrackerEntity* entity = |
| tracker()->GetEntityForGUID(kGuid); |
| ASSERT_THAT(entity, NotNull()); |
| ASSERT_THAT(entity->IsUnsynced(), Eq(false)); |
| |
| // Mark the entity as modified locally. |
| tracker()->IncrementSequenceNumber(entity); |
| ASSERT_THAT(entity->IsUnsynced(), Eq(true)); |
| |
| // Push a remote deletion for the same entity. |
| updates.clear(); |
| updates.push_back(CreateTombstoneResponseData( |
| /*guid=*/kGuid, |
| /*version=*/1)); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // There should have been conflict, and it should have been resolved with the |
| // local version that will be committed later. |
| ASSERT_THAT(tracker()->GetEntityForGUID(kGuid), Eq(entity)); |
| EXPECT_THAT(entity->IsUnsynced(), Eq(true)); |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| EXPECT_THAT(bookmark_bar_node->children().size(), Eq(1u)); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldResolveConflictBetweenLocalDeletionAndRemoteUpdateByRemote) { |
| const std::string kTitle = "title"; |
| const base::GUID kGuid = base::GUID::GenerateRandomV4(); |
| |
| syncer::UpdateResponseDataList updates; |
| |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/0, |
| /*unique_position=*/RandomUniquePosition())); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| const SyncedBookmarkTrackerEntity* entity = |
| tracker()->GetEntityForGUID(kGuid); |
| ASSERT_THAT(entity, NotNull()); |
| ASSERT_THAT(entity->bookmark_node(), NotNull()); |
| ASSERT_THAT(entity->bookmark_node()->guid(), Eq(kGuid)); |
| ASSERT_THAT(entity->IsUnsynced(), Eq(false)); |
| |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| ASSERT_THAT(bookmark_bar_node->children().size(), Eq(1u)); |
| |
| // Mark the entity as deleted locally. |
| tracker()->MarkDeleted(entity); |
| tracker()->IncrementSequenceNumber(entity); |
| ASSERT_THAT(entity->IsUnsynced(), Eq(true)); |
| |
| // Remove the bookmark from the local bookmark model. |
| bookmark_model()->Remove(bookmark_bar_node->children().front().get()); |
| ASSERT_THAT(bookmark_bar_node->children().size(), Eq(0u)); |
| |
| // Push an update for the same entity. |
| updates.clear(); |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/1, |
| /*unique_position=*/RandomUniquePosition())); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // There should have been conflict, and it should have been resolved with the |
| // remote version. The implementation may or may not reuse |entity|, so let's |
| // look it up again. |
| entity = tracker()->GetEntityForGUID(kGuid); |
| ASSERT_THAT(entity, NotNull()); |
| EXPECT_THAT(entity->IsUnsynced(), Eq(false)); |
| EXPECT_THAT(entity->metadata()->is_deleted(), Eq(false)); |
| |
| // The bookmark should have been resurrected. |
| EXPECT_THAT(bookmark_bar_node->children().size(), Eq(1u)); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldResolveConflictBetweenLocalAndRemoteUpdatesWithMatchingThem) { |
| const base::GUID kGuid = base::GUID::GenerateRandomV4(); |
| const std::string kTitle = "title"; |
| |
| syncer::UpdateResponseDataList updates; |
| |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/0, |
| /*unique_position=*/RandomUniquePosition())); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| const SyncedBookmarkTrackerEntity* entity = |
| tracker()->GetEntityForGUID(kGuid); |
| ASSERT_THAT(entity, NotNull()); |
| ASSERT_THAT(entity->IsUnsynced(), Eq(false)); |
| |
| // Mark the entity as modified locally. |
| tracker()->IncrementSequenceNumber(entity); |
| ASSERT_THAT(entity->IsUnsynced(), Eq(true)); |
| |
| // Push an update for the same entity with the same information. |
| updates.clear(); |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/1, |
| /*unique_position=*/RandomUniquePosition())); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| // There should have been conflict but both local and remote updates should |
| // match. The conflict should have been resolved. |
| ASSERT_THAT(tracker()->GetEntityForGUID(kGuid), Eq(entity)); |
| EXPECT_THAT(entity->IsUnsynced(), Eq(false)); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldResolveConflictBetweenLocalAndRemoteUpdatesWithRemote) { |
| const base::GUID kGuid = base::GUID::GenerateRandomV4(); |
| const std::string kTitle = "title"; |
| const std::string kNewRemoteTitle = "remote title"; |
| |
| syncer::UpdateResponseDataList updates; |
| |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/0, |
| /*unique_position=*/RandomUniquePosition())); |
| |
| BookmarkRemoteUpdatesHandler updates_handler(bookmark_model(), |
| favicon_service(), tracker()); |
| updates_handler.Process(updates, /*got_new_encryption_requirements=*/false); |
| |
| const SyncedBookmarkTrackerEntity* entity = |
| tracker()->GetEntityForGUID(kGuid); |
| ASSERT_THAT(entity, NotNull()); |
| ASSERT_THAT(entity->IsUnsynced(), Eq(false)); |
| |
| // Mark the entity as modified locally. |
| tracker()->IncrementSequenceNumber(entity); |
| ASSERT_THAT(entity->IsUnsynced(), Eq(true)); |
| |
| // Push an update for the same entity with a new title. |
| updates.clear(); |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kNewRemoteTitle, |
| /*version=*/1, |
| /*unique_position=*/RandomUniquePosition())); |
| |
| updates_handler.Process(updates, /*got_new_encryption_requirements=*/false); |
| |
| // There should have been conflict, and it should have been resolved with the |
| // remote version. |
| ASSERT_THAT(tracker()->GetEntityForGUID(kGuid), Eq(entity)); |
| EXPECT_THAT(entity->IsUnsynced(), Eq(false)); |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| ASSERT_THAT(bookmark_bar_node->children().size(), Eq(1u)); |
| EXPECT_THAT(bookmark_bar_node->children().front()->GetTitle(), |
| Eq(ASCIIToUTF16(kNewRemoteTitle))); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldReuploadOnEmptyUniquePositionOnUpdateWithSameSpecifics) { |
| base::test::ScopedFeatureList override_features; |
| override_features.InitAndEnableFeature(switches::kSyncReuploadBookmarks); |
| |
| const base::GUID kGuid = base::GUID::GenerateRandomV4(); |
| |
| syncer::UpdateResponseDataList updates; |
| updates.push_back( |
| CreateUpdateResponseData(/*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/"Title", |
| /*version=*/0, |
| /*unique_position=*/RandomUniquePosition())); |
| ASSERT_TRUE(updates.back().entity.specifics.bookmark().has_full_title()); |
| |
| BookmarkRemoteUpdatesHandler(bookmark_model(), favicon_service(), tracker()) |
| .Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| const SyncedBookmarkTrackerEntity* entity = |
| tracker()->GetEntityForGUID(kGuid); |
| ASSERT_THAT(entity, NotNull()); |
| ASSERT_FALSE(entity->IsUnsynced()); |
| |
| // Mimic the case when there is another update but without |unique_position| |
| // in the original specifics. |
| updates.back().entity.is_bookmark_unique_position_in_specifics_preprocessed = |
| true; |
| updates.back().response_version++; |
| BookmarkRemoteUpdatesHandler(bookmark_model(), favicon_service(), tracker()) |
| .Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| EXPECT_TRUE(entity->IsUnsynced()); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldIncrementSequenceNumberOnConflict) { |
| base::test::ScopedFeatureList override_features; |
| override_features.InitAndEnableFeature(switches::kSyncReuploadBookmarks); |
| const base::GUID kGuid = base::GUID::GenerateRandomV4(); |
| const std::string kTitle = "title"; |
| const std::string kNewTitle = "New title"; |
| |
| // Create a local bookmark with unsynced state (it should be unsynced due to |
| // enabled reupload). |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/0, |
| /*unique_position=*/RandomUniquePosition())); |
| updates.back().entity.is_bookmark_unique_position_in_specifics_preprocessed = |
| true; |
| |
| { |
| BookmarkRemoteUpdatesHandler updates_handler(bookmark_model(), |
| favicon_service(), tracker()); |
| updates_handler.Process(updates, /*got_new_encryption_requirements=*/false); |
| } |
| |
| const SyncedBookmarkTrackerEntity* entity = |
| tracker()->GetEntityForGUID(kGuid); |
| ASSERT_THAT(entity, NotNull()); |
| ASSERT_TRUE(entity->IsUnsynced()); |
| |
| // Check that the |entity| is applied an incoming update (by verifying that |
| // the node's title has been changed) and that it will be reuploaded after |
| // conflict resolution. |
| updates.back() |
| .entity.specifics.mutable_bookmark() |
| ->set_legacy_canonicalized_title(kNewTitle); |
| updates.back().entity.specifics.mutable_bookmark()->set_full_title(kNewTitle); |
| updates.back().response_version++; |
| { |
| BookmarkRemoteUpdatesHandler updates_handler(bookmark_model(), |
| favicon_service(), tracker()); |
| updates_handler.Process(updates, /*got_new_encryption_requirements=*/false); |
| } |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| ASSERT_EQ(1u, bookmark_bar_node->children().size()); |
| const bookmarks::BookmarkNode* node = |
| bookmark_bar_node->children().front().get(); |
| EXPECT_EQ(base::UTF16ToUTF8(node->GetTitle()), kNewTitle); |
| EXPECT_TRUE(entity->IsUnsynced()); |
| |
| // Same as above but with the same title in specifics (the local entity |
| // contains the same specifics as the incoming update). |
| updates.back().response_version++; |
| { |
| BookmarkRemoteUpdatesHandler updates_handler(bookmark_model(), |
| favicon_service(), tracker()); |
| updates_handler.Process(updates, /*got_new_encryption_requirements=*/false); |
| } |
| ASSERT_EQ(1u, bookmark_bar_node->children().size()); |
| EXPECT_EQ(bookmark_bar_node->children().front().get(), node); |
| EXPECT_EQ(base::UTF16ToUTF8(node->GetTitle()), kNewTitle); |
| EXPECT_TRUE(entity->IsUnsynced()); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldIncrementSequenceNumberOnUpdate) { |
| base::test::ScopedFeatureList override_features; |
| override_features.InitAndEnableFeature(switches::kSyncReuploadBookmarks); |
| const base::GUID kGuid = base::GUID::GenerateRandomV4(); |
| const std::string kTitle = "title"; |
| const std::string kRemoteTitle = "New title"; |
| |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/0, |
| /*unique_position=*/RandomUniquePosition())); |
| |
| { |
| BookmarkRemoteUpdatesHandler updates_handler(bookmark_model(), |
| favicon_service(), tracker()); |
| updates_handler.Process(updates, /*got_new_encryption_requirements=*/false); |
| } |
| |
| const SyncedBookmarkTrackerEntity* entity = |
| tracker()->GetEntityForGUID(kGuid); |
| ASSERT_THAT(entity, NotNull()); |
| ASSERT_FALSE(entity->IsUnsynced()); |
| |
| // Check reupload on update. |
| updates.back() |
| .entity.specifics.mutable_bookmark() |
| ->set_legacy_canonicalized_title(kRemoteTitle); |
| updates.back().entity.specifics.mutable_bookmark()->set_full_title( |
| kRemoteTitle); |
| updates.back().entity.is_bookmark_unique_position_in_specifics_preprocessed = |
| true; |
| updates.back().response_version++; |
| { |
| BookmarkRemoteUpdatesHandler updates_handler(bookmark_model(), |
| favicon_service(), tracker()); |
| updates_handler.Process(updates, /*got_new_encryption_requirements=*/false); |
| } |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| ASSERT_EQ(1u, bookmark_bar_node->children().size()); |
| const bookmarks::BookmarkNode* node = |
| bookmark_bar_node->children().front().get(); |
| EXPECT_EQ(node->GetTitle(), base::UTF8ToUTF16(kRemoteTitle)); |
| EXPECT_TRUE(entity->IsUnsynced()); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldReuploadBookmarkOnEmptyUniquePosition) { |
| base::test::ScopedFeatureList override_features; |
| override_features.InitAndEnableFeature(switches::kSyncReuploadBookmarks); |
| const std::string kFolder1Title = "folder1"; |
| const std::string kFolder2Title = "folder2"; |
| |
| const base::GUID kFolder1Guid = base::GUID::GenerateRandomV4(); |
| const base::GUID kFolder2Guid = base::GUID::GenerateRandomV4(); |
| |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kFolder1Guid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kFolder1Title, |
| /*version=*/0, |
| /*unique_position=*/RandomUniquePosition())); |
| |
| // Remove |unique_position| field for the first item only. |
| updates.back().entity.is_bookmark_unique_position_in_specifics_preprocessed = |
| true; |
| |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kFolder2Guid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kFolder2Title, |
| /*version=*/0, |
| /*unique_position=*/RandomUniquePosition())); |
| ASSERT_FALSE( |
| updates.back() |
| .entity.is_bookmark_unique_position_in_specifics_preprocessed); |
| |
| updates_handler()->Process(std::move(updates), |
| /*got_new_encryption_requirements=*/false); |
| |
| const SyncedBookmarkTrackerEntity* entity1 = |
| tracker()->GetEntityForGUID(kFolder1Guid); |
| const SyncedBookmarkTrackerEntity* entity2 = |
| tracker()->GetEntityForGUID(kFolder2Guid); |
| ASSERT_THAT(entity1, NotNull()); |
| ASSERT_THAT(entity2, NotNull()); |
| |
| EXPECT_TRUE(entity1->IsUnsynced()); |
| EXPECT_FALSE(entity2->IsUnsynced()); |
| } |
| |
| // Tests that the reflection which doesn't have |unique_position| in specifics |
| // will be reuploaded. |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldReuploadBookmarkOnEmptyUniquePositionForReflection) { |
| base::test::ScopedFeatureList override_features; |
| override_features.InitAndEnableFeature(switches::kSyncReuploadBookmarks); |
| const std::string kFolderTitle = "folder"; |
| const base::GUID kFolderGuid = base::GUID::GenerateRandomV4(); |
| |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model()->bookmark_bar_node(); |
| const bookmarks::BookmarkNode* node = bookmark_model()->AddFolder( |
| bookmark_bar_node, /*index=*/0, base::UTF8ToUTF16(kFolderTitle), |
| /*meta_info=*/nullptr, /*creation_time=*/base::Time::Now(), kFolderGuid); |
| sync_pb::BookmarkModelMetadata model_metadata = |
| CreateMetadataForPermanentNodes(bookmark_model()); |
| sync_pb::BookmarkMetadata* node_metadata = |
| model_metadata.add_bookmarks_metadata(); |
| *node_metadata = CreateNodeMetadata(node, RandomUniquePosition()); |
| const int64_t server_version = node_metadata->metadata().server_version(); |
| |
| std::unique_ptr<SyncedBookmarkTracker> tracker = |
| SyncedBookmarkTracker::CreateFromBookmarkModelAndMetadata( |
| bookmark_model(), std::move(model_metadata)); |
| ASSERT_THAT(tracker, NotNull()); |
| ASSERT_EQ(4u, tracker->GetAllEntities().size()); |
| |
| const SyncedBookmarkTrackerEntity* entity = |
| tracker->GetEntityForGUID(kFolderGuid); |
| ASSERT_THAT(entity, NotNull()); |
| ASSERT_FALSE(entity->IsUnsynced()); |
| |
| syncer::UpdateResponseDataList updates; |
| // Create an update with the same server version as local entity has. This |
| // will simulate processing of reflection. |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kFolderGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kFolderTitle, |
| /*version=*/server_version, |
| /*unique_position=*/RandomUniquePosition())); |
| |
| updates.back().entity.is_bookmark_unique_position_in_specifics_preprocessed = |
| true; |
| |
| BookmarkRemoteUpdatesHandler updates_handler( |
| bookmark_model(), favicon_service(), tracker.get()); |
| updates_handler.Process(std::move(updates), |
| /*got_new_encryption_requirements=*/false); |
| ASSERT_EQ(entity, tracker->GetEntityForGUID(kFolderGuid)); |
| |
| EXPECT_TRUE(entity->IsUnsynced()); |
| } |
| |
| TEST_F(BookmarkRemoteUpdatesHandlerWithInitialMergeTest, |
| ShouldProcessDifferentEntitiesWithSameGuid) { |
| const std::string kServerId1 = "server_id_1"; |
| const std::string kServerId2 = "server_id_2"; |
| |
| const std::string kTitle = "Title"; |
| const base::GUID kGuid = base::GUID::GenerateRandomV4(); |
| |
| // Initialize the model with one node. |
| syncer::UpdateResponseDataList updates; |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/0, |
| /*unique_position=*/RandomUniquePosition())); |
| updates[0].entity.id = kServerId1; |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| ASSERT_THAT(tracker()->TrackedEntitiesCountForTest(), Eq(4U)); |
| ASSERT_THAT(tracker()->GetEntityForSyncId(kServerId1), NotNull()); |
| updates.clear(); |
| |
| // Create two updates having the same GUID, one is a tombstone for the old |
| // |server_id|, another with a new one. The tombstone is processed after the |
| // update and should be ignored due to its server version. |
| updates.push_back(CreateUpdateResponseData( |
| /*guid=*/kGuid, |
| /*parent_guid=*/kBookmarkBarGuid, |
| /*title=*/kTitle, |
| /*version=*/2, RandomUniquePosition())); |
| updates[0].entity.id = kServerId2; |
| updates.push_back(CreateTombstoneResponseData( |
| /*guid=*/kGuid, |
| /*version=*/1)); |
| |
| updates_handler()->Process(updates, |
| /*got_new_encryption_requirements=*/false); |
| |
| EXPECT_THAT(tracker()->TrackedEntitiesCountForTest(), Eq(4U)); |
| EXPECT_THAT(tracker()->GetEntityForSyncId(kServerId1), IsNull()); |
| const SyncedBookmarkTrackerEntity* entity = |
| tracker()->GetEntityForSyncId(kServerId2); |
| EXPECT_THAT(entity, NotNull()); |
| EXPECT_THAT(entity->bookmark_node(), NotNull()); |
| EXPECT_THAT(entity->bookmark_node()->guid(), Eq(kGuid)); |
| } |
| |
| TEST(BookmarkRemoteUpdatesHandlerTest, |
| ShouldComputeRightChildNodeIndexForEmptyParent) { |
| const std::string suffix = syncer::UniquePosition::RandomSuffix(); |
| const syncer::UniquePosition pos1 = |
| syncer::UniquePosition::InitialPosition(suffix); |
| |
| std::unique_ptr<bookmarks::BookmarkModel> bookmark_model = |
| bookmarks::TestBookmarkClient::CreateModel(); |
| std::unique_ptr<SyncedBookmarkTracker> tracker = |
| SyncedBookmarkTracker::CreateFromBookmarkModelAndMetadata( |
| bookmark_model.get(), |
| CreateMetadataForPermanentNodes(bookmark_model.get())); |
| |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model->bookmark_bar_node(); |
| |
| // Should always return 0 for any UniquePosition in the initial state. |
| EXPECT_EQ(0u, BookmarkRemoteUpdatesHandler::ComputeChildNodeIndexForTest( |
| bookmark_bar_node, pos1.ToProto(), tracker.get())); |
| } |
| |
| TEST(BookmarkRemoteUpdatesHandlerTest, ShouldComputeRightChildNodeIndex) { |
| std::unique_ptr<bookmarks::BookmarkModel> bookmark_model = |
| bookmarks::TestBookmarkClient::CreateModel(); |
| |
| const bookmarks::BookmarkNode* bookmark_bar_node = |
| bookmark_model->bookmark_bar_node(); |
| const std::string suffix = syncer::UniquePosition::RandomSuffix(); |
| |
| const syncer::UniquePosition pos1 = |
| syncer::UniquePosition::InitialPosition(suffix); |
| const syncer::UniquePosition pos2 = |
| syncer::UniquePosition::After(pos1, suffix); |
| const syncer::UniquePosition pos3 = |
| syncer::UniquePosition::After(pos2, suffix); |
| |
| // Create 3 nodes using remote update. |
| const bookmarks::BookmarkNode* node1 = bookmark_model->AddFolder( |
| bookmark_bar_node, /*index=*/0, /*title=*/std::u16string()); |
| const bookmarks::BookmarkNode* node2 = bookmark_model->AddFolder( |
| bookmark_bar_node, /*index=*/1, /*title=*/std::u16string()); |
| const bookmarks::BookmarkNode* node3 = bookmark_model->AddFolder( |
| bookmark_bar_node, /*index=*/2, /*title=*/std::u16string()); |
| |
| sync_pb::BookmarkModelMetadata model_metadata = |
| CreateMetadataForPermanentNodes(bookmark_model.get()); |
| *model_metadata.add_bookmarks_metadata() = CreateNodeMetadata(node1, pos1); |
| *model_metadata.add_bookmarks_metadata() = CreateNodeMetadata(node2, pos2); |
| *model_metadata.add_bookmarks_metadata() = CreateNodeMetadata(node3, pos3); |
| |
| std::unique_ptr<SyncedBookmarkTracker> tracker = |
| SyncedBookmarkTracker::CreateFromBookmarkModelAndMetadata( |
| bookmark_model.get(), std::move(model_metadata)); |
| |
| // Check for the same position as existing bookmarks have. In practice this |
| // shouldn't happen. |
| EXPECT_EQ(1u, BookmarkRemoteUpdatesHandler::ComputeChildNodeIndexForTest( |
| bookmark_bar_node, pos1.ToProto(), tracker.get())); |
| EXPECT_EQ(2u, BookmarkRemoteUpdatesHandler::ComputeChildNodeIndexForTest( |
| bookmark_bar_node, pos2.ToProto(), tracker.get())); |
| EXPECT_EQ(3u, BookmarkRemoteUpdatesHandler::ComputeChildNodeIndexForTest( |
| bookmark_bar_node, pos3.ToProto(), tracker.get())); |
| |
| EXPECT_EQ(0u, BookmarkRemoteUpdatesHandler::ComputeChildNodeIndexForTest( |
| bookmark_bar_node, |
| syncer::UniquePosition::Before(pos1, suffix).ToProto(), |
| tracker.get())); |
| EXPECT_EQ(1u, BookmarkRemoteUpdatesHandler::ComputeChildNodeIndexForTest( |
| bookmark_bar_node, |
| syncer::UniquePosition::Between(/*before=*/pos1, |
| /*after=*/pos2, suffix) |
| .ToProto(), |
| tracker.get())); |
| EXPECT_EQ(2u, BookmarkRemoteUpdatesHandler::ComputeChildNodeIndexForTest( |
| bookmark_bar_node, |
| syncer::UniquePosition::Between(/*before=*/pos2, |
| /*after=*/pos3, suffix) |
| .ToProto(), |
| tracker.get())); |
| EXPECT_EQ(3u, BookmarkRemoteUpdatesHandler::ComputeChildNodeIndexForTest( |
| bookmark_bar_node, |
| syncer::UniquePosition::After(pos3, suffix).ToProto(), |
| tracker.get())); |
| } |
| |
| } // namespace |
| |
| } // namespace sync_bookmarks |