blob: 3d9a2f45cdb7d8d48bbd2e474a5b646a84c348b5 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// 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_model_merger.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/logging.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/uuid.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/browser/bookmark_test_util.h"
#include "components/bookmarks/browser/bookmark_uuids.h"
#include "components/bookmarks/test/test_matchers.h"
#include "components/favicon/core/test/mock_favicon_service.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/sync/base/client_tag_hash.h"
#include "components/sync/base/unique_position.h"
#include "components/sync/protocol/entity_metadata.pb.h"
#include "components/sync_bookmarks/bookmark_model_view.h"
#include "components/sync_bookmarks/bookmark_specifics_conversions.h"
#include "components/sync_bookmarks/switches.h"
#include "components/sync_bookmarks/synced_bookmark_tracker.h"
#include "components/sync_bookmarks/synced_bookmark_tracker_entity.h"
#include "components/sync_bookmarks/test_bookmark_model_view.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace sync_bookmarks {
namespace {
using bookmarks::test::IsFolder;
using bookmarks::test::IsFolderWithUuid;
using bookmarks::test::IsUrlBookmark;
using bookmarks::test::IsUrlBookmarkWithUuid;
using testing::_;
using testing::ElementsAre;
using testing::Eq;
using testing::IsEmpty;
using testing::IsNull;
using testing::Ne;
using testing::NotNull;
using testing::UnorderedElementsAre;
MATCHER_P(IsUnsyncedBookmarkEntity, node_matcher, "") {
if (!arg) {
*result_listener << "Got null tracked entity";
return false;
}
if (!arg->IsUnsynced()) {
*result_listener << "Entity not marked as unsynced";
return false;
}
return testing::ExplainMatchResult(NotNull(), arg, result_listener) &&
testing::ExplainMatchResult(node_matcher, arg->bookmark_node(),
result_listener);
}
MATCHER_P(IsTombstone, server_id, "") {
if (!arg) {
*result_listener << "Got null tracked entity";
return false;
}
if (arg->bookmark_node()) {
*result_listener << "Expected tombstone but got tracked entity with title "
<< arg->bookmark_node()->GetTitle();
return false;
}
if (!arg->IsUnsynced()) {
*result_listener << "Tombstone not marked as unsynced";
return false;
}
return testing::ExplainMatchResult(server_id, arg->metadata().server_id(),
result_listener);
}
// Copy of BookmarksUuidDuplicates.
enum class ExpectedBookmarksUuidDuplicates {
kMatchingUrls = 0,
kMatchingFolders = 1,
kDifferentUrls = 2,
kDifferentFolders = 3,
kDifferentTypes = 4,
};
const char kBookmarkBarId[] = "bookmark_bar_id";
const char kBookmarkBarTag[] = "bookmark_bar";
// Fork of enum RemoteBookmarkUpdateError.
enum class ExpectedRemoteBookmarkUpdateError {
kInvalidSpecifics = 1,
kInvalidUniquePosition = 2,
kMissingParentEntity = 4,
kUnexpectedUuid = 9,
kParentNotFolder = 10,
kUnsupportedPermanentFolder = 13,
kDescendantOfRootNodeWithoutPermanentFolder = 14,
kMaxValue = kDescendantOfRootNodeWithoutPermanentFolder,
};
// |*arg| must be of type std::vector<std::unique_ptr<bookmarks::BookmarkNode>>.
MATCHER_P(ElementRawPointersAre, expected_raw_ptr, "") {
if (arg.size() != 1) {
return false;
}
return arg[0].get() == expected_raw_ptr;
}
// |*arg| must be of type std::vector<std::unique_ptr<bookmarks::BookmarkNode>>.
MATCHER_P2(ElementRawPointersAre, expected_raw_ptr0, expected_raw_ptr1, "") {
if (arg.size() != 2) {
return false;
}
return arg[0].get() == expected_raw_ptr0 && arg[1].get() == expected_raw_ptr1;
}
base::Uuid BookmarkBarUuid() {
return base::Uuid::ParseLowercase(bookmarks::kBookmarkBarNodeUuid);
}
// Returns a sync ID mimic-ing what a real server could return, which means it
// generally opaque for the client but deterministic given |uuid|, because the
// sync ID is roughly a hashed UUID, at least in normal circumnstances where the
// UUID is used either as client tag hash or as originator client item ID.
std::string GetFakeServerIdFromUuid(const base::Uuid& uuid) {
// For convenience in tests, |uuid| may refer to permanent nodes too,
// and yet the returned sync ID will honor the sync ID constants for permanent
// nodes.
if (uuid.AsLowercaseString() == bookmarks::kBookmarkBarNodeUuid) {
return kBookmarkBarId;
}
return base::StrCat({"server_id_for_", uuid.AsLowercaseString()});
}
class UpdateResponseDataBuilder {
public:
UpdateResponseDataBuilder(const base::Uuid& uuid,
const base::Uuid& parent_uuid,
const std::string& title,
const syncer::UniquePosition& unique_position) {
data_.id = GetFakeServerIdFromUuid(uuid);
data_.originator_client_item_id = uuid.AsLowercaseString();
sync_pb::BookmarkSpecifics* bookmark_specifics =
data_.specifics.mutable_bookmark();
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();
bookmark_specifics->set_guid(uuid.AsLowercaseString());
bookmark_specifics->set_parent_guid(parent_uuid.AsLowercaseString());
}
UpdateResponseDataBuilder& WithClientTagHash() {
CHECK(!data_.originator_client_item_id.empty());
data_.client_tag_hash = syncer::ClientTagHash::FromUnhashed(
syncer::BOOKMARKS, data_.originator_client_item_id);
data_.originator_client_item_id.clear();
return *this;
}
UpdateResponseDataBuilder& SetUrl(const GURL& url) {
data_.specifics.mutable_bookmark()->set_type(
sync_pb::BookmarkSpecifics::URL);
data_.specifics.mutable_bookmark()->set_url(url.spec());
return *this;
}
UpdateResponseDataBuilder& SetLegacyTitleOnly() {
data_.specifics.mutable_bookmark()->clear_full_title();
return *this;
}
UpdateResponseDataBuilder& SetFavicon(const GURL& favicon_url,
const std::string& favicon_data) {
data_.specifics.mutable_bookmark()->set_icon_url(favicon_url.spec());
data_.specifics.mutable_bookmark()->set_favicon(favicon_data);
return *this;
}
syncer::UpdateResponseData Build() {
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;
}
private:
syncer::EntityData data_;
};
syncer::UpdateResponseData CreateUpdateResponseData(
const base::Uuid& uuid,
const base::Uuid& parent_uuid,
const std::string& title,
const std::string& url,
bool is_folder,
const syncer::UniquePosition& unique_position,
const std::string& icon_url = std::string(),
const std::string& icon_data = std::string()) {
UpdateResponseDataBuilder builder(uuid, parent_uuid, title, unique_position);
if (!is_folder) {
builder.SetUrl(GURL(url));
}
builder.SetFavicon(GURL(icon_url), icon_data);
return builder.Build();
}
syncer::UpdateResponseData CreateBookmarkBarNodeUpdateData() {
syncer::EntityData data;
data.id = kBookmarkBarId;
data.server_defined_unique_tag = kBookmarkBarTag;
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::UniquePosition PositionOf(const bookmarks::BookmarkNode* node,
const SyncedBookmarkTracker& tracker) {
const SyncedBookmarkTrackerEntity* entity =
tracker.GetEntityForBookmarkNode(node);
return syncer::UniquePosition::FromProto(
entity->metadata().unique_position());
}
bool PositionsInTrackerMatchModel(const bookmarks::BookmarkNode* node,
const SyncedBookmarkTracker& tracker) {
if (node->children().empty()) {
return true;
}
syncer::UniquePosition last_pos =
PositionOf(node->children().front().get(), tracker);
for (size_t i = 1; i < node->children().size(); ++i) {
syncer::UniquePosition pos = PositionOf(node->children()[i].get(), tracker);
if (pos.LessThan(last_pos)) {
DLOG(ERROR) << "Position of " << node->children()[i]->GetTitle()
<< " is less than position of "
<< node->children()[i - 1]->GetTitle();
return false;
}
last_pos = pos;
}
return std::ranges::all_of(node->children(), [&tracker](const auto& child) {
return PositionsInTrackerMatchModel(child.get(), tracker);
});
}
std::unique_ptr<SyncedBookmarkTracker> Merge(
syncer::UpdateResponseDataList updates,
BookmarkModelView* bookmark_model) {
std::unique_ptr<SyncedBookmarkTracker> tracker =
SyncedBookmarkTracker::CreateEmpty(sync_pb::DataTypeState());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
BookmarkModelMerger(std::move(updates), bookmark_model, &favicon_service,
tracker.get())
.Merge();
return tracker;
}
static syncer::UniquePosition MakeRandomPosition() {
return syncer::UniquePosition::InitialPosition(
syncer::UniquePosition::RandomSuffix());
}
} // namespace
TEST(BookmarkModelMergerTest, ShouldMergeLocalAndRemoteModels) {
const std::u16string kFolder1Title = u"folder1";
const std::u16string kFolder2Title = u"folder2";
const std::u16string kFolder3Title = u"folder3";
const std::u16string kUrl1Title = u"url1";
const std::u16string kUrl2Title = u"url2";
const std::u16string kUrl3Title = u"url3";
const std::u16string kUrl4Title = u"url4";
const std::string kUrl1 = "http://www.url1.com";
const std::string kUrl2 = "http://www.url2.com";
const std::string kUrl3 = "http://www.url3.com";
const std::string kUrl4 = "http://www.url4.com";
const std::string kAnotherUrl2 = "http://www.another-url2.com";
const base::Uuid kFolder1Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kFolder3Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kUrl1Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kUrl2Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kUrl3Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kUrl4Uuid = base::Uuid::GenerateRandomV4();
// -------- The local model --------
// bookmark_bar
// |- folder 1
// |- url1(http://www.url1.com)
// |- url2(http://www.url2.com)
// |- folder 2
// |- url3(http://www.url3.com)
// |- url4(http://www.url4.com)
TestBookmarkModelView bookmark_model;
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* folder1 = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0, kFolder1Title);
const bookmarks::BookmarkNode* folder2 = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/1, kFolder2Title);
bookmark_model.AddURL(
/*parent=*/folder1, /*index=*/0, kUrl1Title, GURL(kUrl1));
bookmark_model.AddURL(
/*parent=*/folder1, /*index=*/1, kUrl2Title, GURL(kUrl2));
bookmark_model.AddURL(
/*parent=*/folder2, /*index=*/0, kUrl3Title, GURL(kUrl3));
bookmark_model.AddURL(
/*parent=*/folder2, /*index=*/1, kUrl4Title, GURL(kUrl4));
// -------- The remote model --------
// bookmark_bar
// |- folder 1
// |- url1(http://www.url1.com)
// |- url2(http://www.another-url2.com)
// |- folder 3
// |- url3(http://www.url3.com)
// |- url4(http://www.url4.com)
const syncer::UniquePosition::Suffix suffix =
syncer::UniquePosition::RandomSuffix();
syncer::UniquePosition posFolder1 =
syncer::UniquePosition::InitialPosition(suffix);
syncer::UniquePosition posFolder3 =
syncer::UniquePosition::After(posFolder1, suffix);
syncer::UniquePosition posUrl1 =
syncer::UniquePosition::InitialPosition(suffix);
syncer::UniquePosition posUrl2 =
syncer::UniquePosition::After(posUrl1, suffix);
syncer::UniquePosition posUrl3 =
syncer::UniquePosition::InitialPosition(suffix);
syncer::UniquePosition posUrl4 =
syncer::UniquePosition::After(posUrl3, suffix);
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kFolder1Uuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kFolder1Title),
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder1));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUrl1Uuid, /*parent_uuid=*/kFolder1Uuid,
base::UTF16ToUTF8(kUrl1Title), kUrl1,
/*is_folder=*/false, /*unique_position=*/posUrl1));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUrl2Uuid, /*parent_uuid=*/kFolder1Uuid,
base::UTF16ToUTF8(kUrl2Title), kAnotherUrl2,
/*is_folder=*/false, /*unique_position=*/posUrl2));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kFolder3Uuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kFolder3Title),
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder3));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUrl3Uuid, /*parent_uuid=*/kFolder3Uuid,
base::UTF16ToUTF8(kUrl3Title), kUrl3,
/*is_folder=*/false, /*unique_position=*/posUrl3));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUrl4Uuid, /*parent_uuid=*/kFolder3Uuid,
base::UTF16ToUTF8(kUrl4Title), kUrl4,
/*is_folder=*/false, /*unique_position=*/posUrl4));
// -------- The expected merge outcome --------
// bookmark_bar
// |- folder 1
// |- url1(http://www.url1.com)
// |- url2(http://www.another-url2.com)
// |- url2(http://www.url2.com)
// |- folder 3
// |- url3(http://www.url3.com)
// |- url4(http://www.url4.com)
// |- folder 2
// |- url3(http://www.url3.com)
// |- url4(http://www.url4.com)
base::HistogramTester histogram_tester;
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
ASSERT_THAT(bookmark_bar_node->children().size(), Eq(3u));
// Verify Folder 1.
EXPECT_THAT(bookmark_bar_node->children()[0]->GetTitle(), Eq(kFolder1Title));
ASSERT_THAT(bookmark_bar_node->children()[0]->children().size(), Eq(3u));
EXPECT_THAT(bookmark_bar_node->children()[0]->children()[0]->GetTitle(),
Eq(kUrl1Title));
EXPECT_THAT(bookmark_bar_node->children()[0]->children()[0]->url(),
Eq(GURL(kUrl1)));
EXPECT_THAT(bookmark_bar_node->children()[0]->children()[1]->GetTitle(),
Eq(kUrl2Title));
EXPECT_THAT(bookmark_bar_node->children()[0]->children()[1]->url(),
Eq(GURL(kAnotherUrl2)));
EXPECT_THAT(bookmark_bar_node->children()[0]->children()[2]->GetTitle(),
Eq(kUrl2Title));
EXPECT_THAT(bookmark_bar_node->children()[0]->children()[2]->url(),
Eq(GURL(kUrl2)));
// Verify Folder 3.
EXPECT_THAT(bookmark_bar_node->children()[1]->GetTitle(), Eq(kFolder3Title));
ASSERT_THAT(bookmark_bar_node->children()[1]->children().size(), Eq(2u));
EXPECT_THAT(bookmark_bar_node->children()[1]->children()[0]->GetTitle(),
Eq(kUrl3Title));
EXPECT_THAT(bookmark_bar_node->children()[1]->children()[0]->url(),
Eq(GURL(kUrl3)));
EXPECT_THAT(bookmark_bar_node->children()[1]->children()[1]->GetTitle(),
Eq(kUrl4Title));
EXPECT_THAT(bookmark_bar_node->children()[1]->children()[1]->url(),
Eq(GURL(kUrl4)));
// Verify Folder 2.
EXPECT_THAT(bookmark_bar_node->children()[2]->GetTitle(), Eq(kFolder2Title));
ASSERT_THAT(bookmark_bar_node->children()[2]->children().size(), Eq(2u));
EXPECT_THAT(bookmark_bar_node->children()[2]->children()[0]->GetTitle(),
Eq(kUrl3Title));
EXPECT_THAT(bookmark_bar_node->children()[2]->children()[0]->url(),
Eq(GURL(kUrl3)));
EXPECT_THAT(bookmark_bar_node->children()[2]->children()[1]->GetTitle(),
Eq(kUrl4Title));
EXPECT_THAT(bookmark_bar_node->children()[2]->children()[1]->url(),
Eq(GURL(kUrl4)));
EXPECT_THAT(histogram_tester.GetTotalSum(
"Sync.BookmarkModelMerger.UnsyncedEntitiesUponCompletion"),
Eq(4));
// Verify the tracker contents.
EXPECT_THAT(tracker->TrackedEntitiesCountForTest(), Eq(11U));
std::vector<const SyncedBookmarkTrackerEntity*> local_changes =
tracker->GetEntitiesWithLocalChanges();
EXPECT_THAT(local_changes.size(), Eq(4U));
std::vector<const bookmarks::BookmarkNode*> nodes_with_local_changes;
for (const SyncedBookmarkTrackerEntity* local_change : local_changes) {
nodes_with_local_changes.push_back(local_change->bookmark_node());
}
// Verify that url2(http://www.url2.com), Folder 2 and children have
// corresponding update.
EXPECT_THAT(nodes_with_local_changes,
UnorderedElementsAre(
bookmark_bar_node->children()[0]->children()[2].get(),
bookmark_bar_node->children()[2].get(),
bookmark_bar_node->children()[2]->children()[0].get(),
bookmark_bar_node->children()[2]->children()[1].get()));
// Verify positions in tracker.
EXPECT_TRUE(PositionsInTrackerMatchModel(bookmark_bar_node, *tracker));
}
TEST(BookmarkModelMergerTest, ShouldMergeRemoteReorderToLocalModel) {
const std::u16string kFolder1Title = u"folder1";
const std::u16string kFolder2Title = u"folder2";
const std::u16string kFolder3Title = u"folder3";
const base::Uuid kFolder1Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kFolder2Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kFolder3Uuid = base::Uuid::GenerateRandomV4();
// -------- The local model --------
// bookmark_bar
// |- folder 1
// |- folder 2
// |- folder 3
TestBookmarkModelView bookmark_model;
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0, kFolder1Title);
bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/1, kFolder2Title);
bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/2, kFolder3Title);
// -------- The remote model --------
// bookmark_bar
// |- folder 1
// |- folder 3
// |- folder 2
const syncer::UniquePosition::Suffix suffix =
syncer::UniquePosition::RandomSuffix();
syncer::UniquePosition posFolder1 =
syncer::UniquePosition::InitialPosition(suffix);
syncer::UniquePosition posFolder3 =
syncer::UniquePosition::After(posFolder1, suffix);
syncer::UniquePosition posFolder2 =
syncer::UniquePosition::After(posFolder3, suffix);
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kFolder1Uuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kFolder1Title),
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder1));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kFolder2Uuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kFolder2Title),
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder2));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kFolder3Uuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kFolder3Title),
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder3));
// -------- The expected merge outcome --------
// bookmark_bar
// |- folder 1
// |- folder 3
// |- folder 2
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
ASSERT_THAT(bookmark_bar_node->children().size(), Eq(3u));
EXPECT_THAT(bookmark_bar_node->children()[0]->GetTitle(), Eq(kFolder1Title));
EXPECT_THAT(bookmark_bar_node->children()[1]->GetTitle(), Eq(kFolder3Title));
EXPECT_THAT(bookmark_bar_node->children()[2]->GetTitle(), Eq(kFolder2Title));
// Verify the tracker contents.
EXPECT_THAT(tracker->TrackedEntitiesCountForTest(), Eq(4U));
// There should be no local changes.
std::vector<const SyncedBookmarkTrackerEntity*> local_changes =
tracker->GetEntitiesWithLocalChanges();
EXPECT_THAT(local_changes.size(), Eq(0U));
// Verify positions in tracker.
EXPECT_TRUE(PositionsInTrackerMatchModel(bookmark_bar_node, *tracker));
}
TEST(BookmarkModelMergerTest, ShouldIgnoreManagedNodes) {
auto client = std::make_unique<bookmarks::TestBookmarkClient>();
bookmarks::BookmarkNode* managed_node = client->EnableManagedNode();
TestBookmarkModelView view(
TestBookmarkModelView::ViewType::kLocalOrSyncableNodes,
std::move(client));
const bookmarks::BookmarkNode* unsyncable_node =
view.underlying_model()->AddURL(/*parent=*/managed_node, /*index=*/0,
u"Title", GURL("http://www.url.com"));
ASSERT_FALSE(view.IsNodeSyncable(unsyncable_node));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(syncer::UpdateResponseDataList(), &view);
ASSERT_THAT(tracker, NotNull());
EXPECT_THAT(tracker->GetEntityForBookmarkNode(unsyncable_node), IsNull());
EXPECT_THAT(tracker->GetEntitiesWithLocalChanges(), IsEmpty());
EXPECT_THAT(managed_node->children().size(), Eq(1));
}
TEST(BookmarkModelMergerTest, ShouldIgnoreUnsyncableNodes) {
base::test::ScopedFeatureList override_features{
switches::kSyncEnableBookmarksInTransportMode};
TestBookmarkModelView view(TestBookmarkModelView::ViewType::kAccountNodes);
view.EnsurePermanentNodesExist();
const bookmarks::BookmarkNode* unsyncable_node =
view.underlying_model()->AddURL(
/*parent=*/view.underlying_model()->bookmark_bar_node(), /*index=*/0,
u"Title", GURL("http://www.url.com"));
ASSERT_FALSE(view.IsNodeSyncable(unsyncable_node));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(syncer::UpdateResponseDataList(), &view);
ASSERT_THAT(tracker, NotNull());
EXPECT_THAT(tracker->GetEntityForBookmarkNode(unsyncable_node), IsNull());
EXPECT_THAT(tracker->GetEntitiesWithLocalChanges(), IsEmpty());
EXPECT_THAT(view.underlying_model()->bookmark_bar_node()->children().size(),
Eq(1));
}
// Regression test for crbug.com/329278277. A UUID collision with an unsyncable
// node is a common scenario for the case where BookmarkModelMerger is being
// exercised for account bookmarks, while local unsyncable bookmarks contain an
// exact copy of the server-side updates as a result of sync-the-feature having
// been previously turned on and later off.
TEST(BookmarkModelMergerTest, ShouldIgnoreUnsyncableNodeWithCollidingUuid) {
base::test::ScopedFeatureList override_features{
switches::kSyncEnableBookmarksInTransportMode};
TestBookmarkModelView view(TestBookmarkModelView::ViewType::kAccountNodes);
view.EnsurePermanentNodesExist();
const bookmarks::BookmarkNode* unsyncable_node =
view.underlying_model()->AddURL(
/*parent=*/view.underlying_model()->bookmark_bar_node(), /*index=*/0,
u"Title", GURL("http://www.foo.com"));
ASSERT_FALSE(view.IsNodeSyncable(unsyncable_node));
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
unsyncable_node->uuid(), /*parent_uuid=*/BookmarkBarUuid(), "Title",
/*url=*/"http://www.bar.com",
/*is_folder=*/false,
syncer::UniquePosition::InitialPosition(
syncer::UniquePosition::RandomSuffix())));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &view);
ASSERT_THAT(tracker, NotNull());
EXPECT_THAT(tracker->GetEntityForBookmarkNode(unsyncable_node), IsNull());
EXPECT_THAT(tracker->GetEntitiesWithLocalChanges(), IsEmpty());
EXPECT_THAT(view.underlying_model()->bookmark_bar_node()->children().size(),
Eq(1));
}
TEST(BookmarkModelMergerTest, ShouldMergeFaviconsForRemoteNodesOnly) {
const std::u16string kTitle1 = u"title1";
const GURL kUrl1("http://www.url1.com");
// -------- The local model --------
// bookmark_bar
// |- title 1
TestBookmarkModelView bookmark_model;
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
bookmark_model.AddURL(
/*parent=*/bookmark_bar_node, /*index=*/0, kTitle1, kUrl1);
// -------- The remote model --------
// bookmark_bar
// |- title 2
const std::u16string kTitle2 = u"title2";
const base::Uuid kUuid2 = base::Uuid::GenerateRandomV4();
const GURL kUrl2("http://www.url2.com");
const GURL kIcon2Url("http://www.icon-url.com");
syncer::UniquePosition pos2 = syncer::UniquePosition::InitialPosition(
syncer::UniquePosition::RandomSuffix());
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid2, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle2), kUrl2.spec(),
/*is_folder=*/false, /*unique_position=*/pos2, kIcon2Url.spec(),
/*icon_data=*/"PNG"));
// -------- The expected merge outcome --------
// bookmark_bar
// |- title 2
// |- title 1
std::unique_ptr<SyncedBookmarkTracker> tracker =
SyncedBookmarkTracker::CreateEmpty(sync_pb::DataTypeState());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
// Favicon should be set for the remote node.
EXPECT_CALL(favicon_service, AddPageNoVisitForBookmark(kUrl2, kTitle2));
EXPECT_CALL(favicon_service, MergeFavicon(kUrl2, _, _, _, _));
BookmarkModelMerger(std::move(updates), &bookmark_model, &favicon_service,
tracker.get())
.Merge();
}
// This tests that canonical titles produced by legacy clients are properly
// matched. Legacy clients append blank space to empty titles.
TEST(BookmarkModelMergerTest,
ShouldMergeLocalAndRemoteNodesWhenRemoteHasLegacyCanonicalTitle) {
const std::u16string kLocalTitle = u"";
const std::string kRemoteTitle = " ";
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* folder = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0, kLocalTitle);
ASSERT_TRUE(folder);
// -------- The remote model --------
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(
UpdateResponseDataBuilder(/*uuid=*/kUuid,
/*parent_uuid=*/BookmarkBarUuid(), kRemoteTitle,
/*unique_position=*/MakeRandomPosition())
.SetLegacyTitleOnly()
.Build());
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
// Both titles should have matched against each other and only node is in the
// model and the tracker.
EXPECT_THAT(bookmark_bar_node->children().size(), Eq(1u));
EXPECT_THAT(tracker->TrackedEntitiesCountForTest(), Eq(2U));
}
// This tests that truncated titles produced by legacy clients are properly
// matched.
TEST(BookmarkModelMergerTest,
ShouldMergeLocalAndRemoteNodesWhenRemoteHasLegacyTruncatedTitle) {
const std::u16string kLocalLongTitle =
u"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs"
u"t"
"uvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN"
"OPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh"
"ijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB"
"CDEFGHIJKLMNOPQRSTUVWXYZ";
const std::string kRemoteTruncatedTitle =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrst"
"uvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN"
"OPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh"
"ijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTU";
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* folder = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0, kLocalLongTitle);
ASSERT_TRUE(folder);
// -------- The remote model --------
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(), kRemoteTruncatedTitle,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/MakeRandomPosition()));
std::unique_ptr<SyncedBookmarkTracker> tracker =
SyncedBookmarkTracker::CreateEmpty(sync_pb::DataTypeState());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
BookmarkModelMerger(std::move(updates), &bookmark_model, &favicon_service,
tracker.get())
.Merge();
// Both titles should have matched against each other and only node is in the
// model and the tracker.
EXPECT_THAT(bookmark_bar_node->children().size(), Eq(1u));
EXPECT_THAT(tracker->TrackedEntitiesCountForTest(), Eq(2U));
}
TEST(BookmarkModelMergerTest,
ShouldMergeNodesWhenRemoteHasLegacyTruncatedTitleInFullTitle) {
const std::u16string kLocalLongTitle(300, 'A');
const std::string kRemoteTruncatedFullTitle(255, 'A');
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* folder = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0, kLocalLongTitle);
ASSERT_TRUE(folder);
// -------- The remote model --------
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(),
kRemoteTruncatedFullTitle,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/MakeRandomPosition()));
updates.back().entity.specifics.mutable_bookmark()->set_full_title(
kRemoteTruncatedFullTitle);
std::unique_ptr<SyncedBookmarkTracker> tracker =
SyncedBookmarkTracker::CreateEmpty(sync_pb::DataTypeState());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
BookmarkModelMerger(std::move(updates), &bookmark_model, &favicon_service,
tracker.get())
.Merge();
// Both titles should have matched against each other and only node is in the
// model and the tracker.
EXPECT_THAT(bookmark_bar_node->children().size(), Eq(1u));
EXPECT_THAT(tracker->TrackedEntitiesCountForTest(), Eq(2U));
}
// This test checks that local node with truncated title will merge with remote
// node which has full title.
TEST(BookmarkModelMergerTest,
ShouldMergeLocalAndRemoteNodesWhenLocalHasLegacyTruncatedTitle) {
const std::string kRemoteFullTitle(300, 'A');
const std::string kLocalTruncatedTitle(255, 'A');
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* folder = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0,
base::ASCIIToUTF16(kLocalTruncatedTitle));
ASSERT_TRUE(folder);
// -------- The remote model --------
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(),
sync_bookmarks::FullTitleToLegacyCanonicalizedTitle(kRemoteFullTitle),
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/MakeRandomPosition()));
ASSERT_EQ(
kLocalTruncatedTitle,
updates.back().entity.specifics.bookmark().legacy_canonicalized_title());
updates.back().entity.specifics.mutable_bookmark()->set_full_title(
kRemoteFullTitle);
std::unique_ptr<SyncedBookmarkTracker> tracker =
SyncedBookmarkTracker::CreateEmpty(sync_pb::DataTypeState());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
BookmarkModelMerger(std::move(updates), &bookmark_model, &favicon_service,
tracker.get())
.Merge();
// Both titles should have matched against each other and only node is in the
// model and the tracker.
EXPECT_THAT(bookmark_bar_node->children().size(), Eq(1u));
EXPECT_THAT(tracker->TrackedEntitiesCountForTest(), Eq(2U));
}
TEST(BookmarkModelMergerTest, ShouldMergeAndUseRemoteUuid) {
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
const std::u16string kTitle = u"Title";
const base::Uuid kRemoteUuid = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* folder = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0, kTitle);
ASSERT_TRUE(folder);
// -------- The remote model --------
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kRemoteUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle),
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/MakeRandomPosition()));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
// Node should have been replaced and UUID should be set to that stored in the
// specifics.
ASSERT_EQ(bookmark_bar_node->children().size(), 1u);
const bookmarks::BookmarkNode* bookmark =
bookmark_model.bookmark_bar_node()->children()[0].get();
EXPECT_EQ(bookmark->uuid(), kRemoteUuid);
EXPECT_THAT(tracker->GetEntityForBookmarkNode(bookmark), NotNull());
}
TEST(BookmarkModelMergerTest,
ShouldMergeAndKeepOldUuidWhenRemoteUuidIsInvalid) {
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
const std::u16string kTitle = u"Title";
TestBookmarkModelView bookmark_model;
// -------- The local model --------
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* folder = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0, kTitle);
ASSERT_TRUE(folder);
const base::Uuid old_uuid = folder->uuid();
// -------- The remote model --------
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/base::Uuid::GenerateRandomV4(),
/*parent_uuid=*/BookmarkBarUuid(), base::UTF16ToUTF8(kTitle),
/*url=*/std::string(),
/*is_folder=*/true,
/*unique_position=*/MakeRandomPosition()));
updates.back().entity.specifics.mutable_bookmark()->set_guid("invalid_guid");
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
// Node should not have been replaced and UUID should not have been set to
// that stored in the specifics, as it was invalid.
ASSERT_EQ(bookmark_bar_node->children().size(), 1u);
const bookmarks::BookmarkNode* bookmark =
bookmark_model.bookmark_bar_node()->children()[0].get();
EXPECT_EQ(bookmark->uuid(), old_uuid);
EXPECT_THAT(tracker->GetEntityForBookmarkNode(bookmark), NotNull());
}
TEST(BookmarkModelMergerTest, ShouldMergeBookmarkByUuid) {
const std::u16string kLocalTitle = u"Title 1";
const std::string kRemoteTitle = "Title 2";
const std::string kUrl = "http://www.foo.com/";
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
// bookmark_bar
// | - bookmark(kUuid/kLocalTitle)
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* bookmark = bookmark_model.AddURL(
/*parent=*/bookmark_bar_node, /*index=*/0, kLocalTitle, GURL(kUrl),
/*meta_info=*/nullptr, base::Time::Now(), kUuid);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// | - bookmark(kUuid/kRemoteTitle)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(), kRemoteTitle,
/*url=*/kUrl,
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
// -------- The merged model --------
// bookmark_bar
// |- bookmark(kUuid/kRemoteTitle)
// Node should have been merged.
EXPECT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
EXPECT_EQ(bookmark->GetTitle(), base::UTF8ToUTF16(kRemoteTitle));
EXPECT_THAT(tracker->GetEntityForBookmarkNode(bookmark), NotNull());
}
TEST(BookmarkModelMergerTest, ShouldMergeBookmarkByUuidAndReparent) {
const std::u16string kLocalTitle = u"Title 1";
const std::string kRemoteTitle = "Title 2";
const std::string kUrl = "http://www.foo.com/";
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
// bookmark_bar
// | - folder
// | - bookmark(kUuid)
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* folder = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0, u"Folder Title");
const bookmarks::BookmarkNode* bookmark = bookmark_model.AddURL(
/*parent=*/folder, /*index=*/0, kLocalTitle, GURL(kUrl),
/*meta_info=*/nullptr, base::Time::Now(), kUuid);
ASSERT_TRUE(folder);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(folder));
ASSERT_THAT(folder->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// |- bookmark(kUuid)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(), kRemoteTitle,
/*url=*/kUrl,
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
// -------- The merged model --------
// bookmark_bar
// | - bookmark(kUuid/kRemoteTitle)
// | - folder
// Node should have been merged and the local node should have been
// reparented.
EXPECT_THAT(bookmark_bar_node->children(),
ElementRawPointersAre(bookmark, folder));
EXPECT_EQ(folder->children().size(), 0u);
EXPECT_EQ(bookmark->GetTitle(), base::UTF8ToUTF16(kRemoteTitle));
EXPECT_THAT(tracker->GetEntityForBookmarkNode(bookmark), NotNull());
EXPECT_THAT(tracker->GetEntityForBookmarkNode(folder), NotNull());
}
TEST(BookmarkModelMergerTest, ShouldMergeFolderByUuidAndNotSemantics) {
const std::string kFolderId = "Folder Id";
const std::u16string kTitle1 = u"Title 1";
const std::u16string kTitle2 = u"Title 2";
const std::string kUrl = "http://www.foo.com/";
const base::Uuid kUuid1 = base::Uuid::GenerateRandomV4();
const base::Uuid kUuid2 = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
// bookmark_bar
// | - folder 1 (kUuid1/kTitle1)
// | - folder 2 (kUuid2/kTitle2)
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* folder1 = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0, kTitle1,
/*meta_info=*/nullptr, /*creation_time=*/base::Time::Now(), kUuid1);
const bookmarks::BookmarkNode* folder2 = bookmark_model.AddFolder(
/*parent=*/folder1, /*index=*/0, kTitle2,
/*meta_info=*/nullptr, /*creation_time=*/base::Time::Now(), kUuid2);
ASSERT_TRUE(folder1);
ASSERT_TRUE(folder2);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(folder1));
ASSERT_THAT(folder1->children(), ElementRawPointersAre(folder2));
// -------- The remote model --------
// bookmark_bar
// | - folder (kUuid2/kTitle1)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
// Add a remote folder to correspond to the local folder by UUID and
// semantics.
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid2, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle1),
/*url=*/"",
/*is_folder=*/true,
/*unique_position=*/MakeRandomPosition()));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
// -------- The merged model --------
// bookmark_bar
// | - folder 2 (kUuid2/kTitle1)
// | - folder 1 (kUuid1/kTitle1)
// Node should have been merged with its UUID match.
EXPECT_THAT(bookmark_bar_node->children(),
ElementRawPointersAre(folder2, folder1));
EXPECT_EQ(folder1->uuid(), kUuid1);
EXPECT_EQ(folder1->GetTitle(), kTitle1);
EXPECT_EQ(folder1->children().size(), 0u);
EXPECT_EQ(folder2->uuid(), kUuid2);
EXPECT_EQ(folder2->GetTitle(), kTitle1);
EXPECT_THAT(tracker->GetEntityForBookmarkNode(folder1), NotNull());
EXPECT_THAT(tracker->GetEntityForBookmarkNode(folder2), NotNull());
}
TEST(BookmarkModelMergerTest, ShouldIgnoreChildrenForNonFolderNodes) {
const std::string kChildId = "child_id";
const std::u16string kParentTitle = u"Parent Title";
const std::u16string kChildTitle = u"Child Title";
const base::Uuid kUuid1 = base::Uuid::GenerateRandomV4();
const base::Uuid kUuid2 = base::Uuid::GenerateRandomV4();
const std::string kUrl1 = "http://www.foo.com/";
const std::string kUrl2 = "http://www.bar.com/";
// -------- The remote model --------
// bookmark_bar
// | - bookmark (kUuid1/kParentTitle, not a folder)
// | - bookmark
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
const syncer::UniquePosition::Suffix suffix =
syncer::UniquePosition::RandomSuffix();
const syncer::UniquePosition pos1 =
syncer::UniquePosition::InitialPosition(suffix);
const syncer::UniquePosition pos2 =
syncer::UniquePosition::After(pos1, suffix);
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid1, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kParentTitle),
/*url=*/kUrl1,
/*is_folder=*/false,
/*unique_position=*/pos1));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid2, /*parent_uuid=*/kUuid1, base::UTF16ToUTF8(kChildTitle),
/*url=*/kUrl2,
/*is_folder=*/false,
/*unique_position=*/pos2));
TestBookmarkModelView bookmark_model;
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
// -------- The merged model --------
// bookmark_bar
// | - bookmark (kUuid1/kParentTitle)
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
ASSERT_EQ(bookmark_bar_node->children().size(), 1u);
EXPECT_EQ(bookmark_bar_node->children()[0]->uuid(), kUuid1);
EXPECT_EQ(bookmark_bar_node->children()[0]->GetTitle(), kParentTitle);
EXPECT_EQ(bookmark_bar_node->children()[0]->children().size(), 0u);
EXPECT_EQ(tracker->TrackedEntitiesCountForTest(), 2U);
}
TEST(
BookmarkModelMergerTest,
ShouldIgnoreFolderSemanticsMatchAndLaterMatchByUuidWithSemanticsNodeFirst) {
const std::string kFolderId1 = "Folder Id 1";
const std::string kFolderId2 = "Folder Id 2";
const std::u16string kOriginalTitle = u"Original Title";
const std::u16string kNewTitle = u"New Title";
const base::Uuid kUuid1 = base::Uuid::GenerateRandomV4();
const base::Uuid kUuid2 = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
// bookmark_bar
// | - folder (kUuid1/kOriginalTitle)
// | - bookmark
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* folder = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0, kOriginalTitle,
/*meta_info=*/nullptr,
/*creation_time=*/base::Time::Now(), kUuid1);
const bookmarks::BookmarkNode* bookmark = bookmark_model.AddURL(
/*parent=*/folder, /*index=*/0, u"Bookmark Title",
GURL("http://foo.com/"));
ASSERT_TRUE(folder);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(folder));
ASSERT_THAT(folder->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// | - folder (kUuid2/kOriginalTitle)
// | - folder (kUuid1/kNewTitle)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
const syncer::UniquePosition::Suffix suffix =
syncer::UniquePosition::RandomSuffix();
syncer::UniquePosition pos1 = syncer::UniquePosition::InitialPosition(suffix);
syncer::UniquePosition pos2 = syncer::UniquePosition::After(pos1, suffix);
// Add a remote folder to correspond to the local folder by semantics and not
// UUID.
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid2, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kOriginalTitle),
/*url=*/"",
/*is_folder=*/true,
/*unique_position=*/pos1));
// Add a remote folder to correspond to the local folder by UUID and not
// semantics.
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid1, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kNewTitle),
/*url=*/"",
/*is_folder=*/true,
/*unique_position=*/pos2));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
// -------- The merged model --------
// bookmark_bar
// | - folder (kUuid2/kOriginalTitle)
// | - folder (kUuid1/kNewTitle)
// | - bookmark
// Node should have been merged with its UUID match.
ASSERT_EQ(bookmark_bar_node->children().size(), 2u);
EXPECT_EQ(bookmark_bar_node->children()[0]->uuid(), kUuid2);
EXPECT_EQ(bookmark_bar_node->children()[0]->GetTitle(), kOriginalTitle);
EXPECT_EQ(bookmark_bar_node->children()[0]->children().size(), 0u);
EXPECT_EQ(bookmark_bar_node->children()[1]->uuid(), kUuid1);
EXPECT_EQ(bookmark_bar_node->children()[1]->GetTitle(), kNewTitle);
EXPECT_EQ(bookmark_bar_node->children()[1]->children().size(), 1u);
EXPECT_THAT(tracker->TrackedEntitiesCountForTest(), Eq(4U));
}
TEST(BookmarkModelMergerTest,
ShouldIgnoreFolderSemanticsMatchAndLaterMatchByUuidWithUuidNodeFirst) {
const std::string kFolderId1 = "Folder Id 1";
const std::string kFolderId2 = "Folder Id 2";
const std::u16string kOriginalTitle = u"Original Title";
const std::u16string kNewTitle = u"New Title";
const base::Uuid kUuid1 = base::Uuid::GenerateRandomV4();
const base::Uuid kUuid2 = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
// bookmark_bar
// | - folder (kUuid1/kOriginalTitle)
// | - bookmark
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* folder = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0, kOriginalTitle,
/*meta_info=*/nullptr,
/*creation_time=*/base::Time::Now(), kUuid1);
const bookmarks::BookmarkNode* bookmark = bookmark_model.AddURL(
/*parent=*/folder, /*index=*/0, u"Bookmark Title",
GURL("http://foo.com/"));
ASSERT_TRUE(folder);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(folder));
ASSERT_THAT(folder->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// | - folder (kUuid1/kNewTitle)
// | - folder (kUuid2/kOriginalTitle)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
const syncer::UniquePosition::Suffix suffix =
syncer::UniquePosition::RandomSuffix();
syncer::UniquePosition pos1 = syncer::UniquePosition::InitialPosition(suffix);
syncer::UniquePosition pos2 = syncer::UniquePosition::After(pos1, suffix);
// Add a remote folder to correspond to the local folder by UUID and not
// semantics.
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid1, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kNewTitle),
/*url=*/"",
/*is_folder=*/true,
/*unique_position=*/pos1));
// Add a remote folder to correspond to the local folder by
// semantics and not UUID.
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid2, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kOriginalTitle),
/*url=*/"",
/*is_folder=*/true,
/*unique_position=*/pos2));
Merge(std::move(updates), &bookmark_model);
// -------- The merged model --------
// bookmark_bar
// | - folder (kUuid1/kNewTitle)
// | - folder (kUuid2/kOriginalTitle)
// Node should have been merged with its UUID match.
ASSERT_EQ(bookmark_bar_node->children().size(), 2u);
EXPECT_EQ(bookmark_bar_node->children()[0]->uuid(), kUuid1);
EXPECT_EQ(bookmark_bar_node->children()[0]->GetTitle(), kNewTitle);
EXPECT_EQ(bookmark_bar_node->children()[0]->children().size(), 1u);
EXPECT_EQ(bookmark_bar_node->children()[1]->uuid(), kUuid2);
EXPECT_EQ(bookmark_bar_node->children()[1]->GetTitle(), kOriginalTitle);
EXPECT_EQ(bookmark_bar_node->children()[1]->children().size(), 0u);
}
TEST(BookmarkModelMergerTest, ShouldReplaceBookmarkUuidWithConflictingURLs) {
const std::u16string kTitle = u"Title";
const std::string kUrl1 = "http://www.foo.com/";
const std::string kUrl2 = "http://www.bar.com/";
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
// bookmark_bar
// | - bookmark (kUuid/kUril1)
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* bookmark = bookmark_model.AddURL(
/*parent=*/bookmark_bar_node, /*index=*/0, kTitle, GURL(kUrl1),
/*meta_info=*/nullptr, base::Time::Now(), kUuid);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// | - bookmark (kUuid/kUrl2)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData( // Remote B
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle),
/*url=*/kUrl2,
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
Merge(std::move(updates), &bookmark_model);
// -------- The merged model --------
// bookmark_bar
// | - bookmark (kUuid/kUrl2)
// | - bookmark ([new UUID]/kUrl1)
// Conflicting node UUID should have been replaced.
ASSERT_EQ(bookmark_bar_node->children().size(), 2u);
EXPECT_EQ(bookmark_bar_node->children()[0]->uuid(), kUuid);
EXPECT_EQ(bookmark_bar_node->children()[0]->url(), kUrl2);
EXPECT_NE(bookmark_bar_node->children()[1]->uuid(), kUuid);
EXPECT_EQ(bookmark_bar_node->children()[1]->url(), kUrl1);
}
TEST(BookmarkModelMergerTest, ShouldReplaceBookmarkUuidWithConflictingTypes) {
const std::u16string kTitle = u"Title";
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
// bookmark_bar
// | - bookmark (kUuid)
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* bookmark = bookmark_model.AddURL(
/*parent=*/bookmark_bar_node, /*index=*/0, kTitle,
GURL("http://www.foo.com/"), /*meta_info=*/nullptr, base::Time::Now(),
kUuid);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// | - folder(kUuid)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData( // Remote B
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle),
/*url=*/"",
/*is_folder=*/true,
/*unique_position=*/MakeRandomPosition()));
Merge(std::move(updates), &bookmark_model);
// -------- The merged model --------
// bookmark_bar
// | - folder (kUuid)
// | - bookmark ([new UUID])
// Conflicting node UUID should have been replaced.
ASSERT_EQ(bookmark_bar_node->children().size(), 2u);
EXPECT_EQ(bookmark_bar_node->children()[0]->uuid(), kUuid);
EXPECT_TRUE(bookmark_bar_node->children()[0]->is_folder());
EXPECT_NE(bookmark_bar_node->children()[1]->uuid(), kUuid);
EXPECT_FALSE(bookmark_bar_node->children()[1]->is_folder());
}
TEST(BookmarkModelMergerTest,
ShouldReplaceBookmarkUuidWithConflictingTypesAndLocalChildren) {
const base::Uuid kUuid1 = base::Uuid::GenerateRandomV4();
const base::Uuid kUuid2 = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
// bookmark_bar
// | - folder (kUuid1)
// | - bookmark (kUuid2)
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* folder = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0, u"Folder Title",
/*meta_info=*/nullptr, /*creation_time=*/base::Time::Now(), kUuid1);
const bookmarks::BookmarkNode* bookmark = bookmark_model.AddURL(
/*parent=*/folder, /*index=*/0, u"Foo's title", GURL("http://foo.com"),
/*meta_info=*/nullptr, /*creation_time=*/base::Time::Now(), kUuid2);
ASSERT_TRUE(folder);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(folder));
ASSERT_THAT(folder->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// | - bookmark (kUuid1)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid1, /*parent_uuid=*/BookmarkBarUuid(), "Bar's title",
"http://bar.com/",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
Merge(std::move(updates), &bookmark_model);
// -------- The merged model --------
// bookmark_bar
// | - bookmark (kUuid1)
// | - folder ([new UUID])
// | - bookmark (kUuid2)
// Conflicting node UUID should have been replaced.
ASSERT_EQ(bookmark_bar_node->children().size(), 2u);
EXPECT_EQ(bookmark_bar_node->children()[0]->uuid(), kUuid1);
EXPECT_NE(bookmark_bar_node->children()[1]->uuid(), kUuid1);
EXPECT_NE(bookmark_bar_node->children()[1]->uuid(), kUuid2);
EXPECT_FALSE(bookmark_bar_node->children()[0]->is_folder());
EXPECT_TRUE(bookmark_bar_node->children()[1]->is_folder());
EXPECT_EQ(bookmark_bar_node->children()[1]->children().size(), 1u);
EXPECT_FALSE(bookmark_bar_node->children()[1]->children()[0]->is_folder());
EXPECT_EQ(bookmark_bar_node->children()[1]->children()[0]->uuid(), kUuid2);
}
// Tests that the UUID-based matching algorithm handles well the case where a
// local bookmark matches a remote bookmark that is orphan. In this case the
// remote node should be ignored and the local bookmark included in the merged
// tree.
TEST(BookmarkModelMergerTest, ShouldIgnoreRemoteUuidIfOrphanNode) {
const std::string kInexistentParentId = "InexistentParentId";
const std::u16string kTitle = u"Title";
const std::string kUrl = "http://www.foo.com/";
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
const base::Uuid kInexistentParentUuid = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
// bookmark_bar
// | - bookmark(kUuid/kTitle)
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* bookmark = bookmark_model.AddURL(
/*parent=*/bookmark_bar_node, /*index=*/0, kTitle, GURL(kUrl),
/*meta_info=*/nullptr, base::Time::Now(), kUuid);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// Orphan node: bookmark(kUuid/kTitle)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/kInexistentParentUuid,
base::UTF16ToUTF8(kTitle),
/*url=*/kUrl,
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
// -------- The merged model --------
// bookmark_bar
// |- bookmark(kUuid/kTitle)
// The local node should have been tracked.
EXPECT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
EXPECT_EQ(bookmark->GetTitle(), kTitle);
EXPECT_THAT(tracker->GetEntityForBookmarkNode(bookmark), NotNull());
EXPECT_THAT(tracker->GetEntityForUuid(kUuid), NotNull());
EXPECT_THAT(tracker->GetEntityForUuid(kInexistentParentUuid), IsNull());
}
// Tests that the UUID-based matching algorithm handles well the case where a
// local bookmark matches a remote bookmark that contains invalid specifics
// (e.g. invalid URL). In this case the remote node should be ignored and the
// local bookmark included in the merged tree.
TEST(BookmarkModelMergerTest, ShouldIgnoreRemoteUuidIfInvalidSpecifics) {
const std::u16string kTitle = u"Title";
const std::string kLocalUrl = "http://www.foo.com/";
const std::string kInvalidUrl = "invalidurl";
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
// bookmark_bar
// | - bookmark(kUuid/kLocalUrl/kTitle)
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* bookmark = bookmark_model.AddURL(
/*parent=*/bookmark_bar_node, /*index=*/0, kTitle, GURL(kLocalUrl),
/*meta_info=*/nullptr, base::Time::Now(), kUuid);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// | - bookmark (kUuid/kInvalidURL/kTitle)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle),
/*url=*/kInvalidUrl,
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
// -------- The merged model --------
// bookmark_bar
// |- bookmark(kUuid/kLocalUrl/kTitle)
// The local node should have been tracked.
EXPECT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
EXPECT_EQ(bookmark->url(), GURL(kLocalUrl));
EXPECT_EQ(bookmark->GetTitle(), kTitle);
EXPECT_THAT(tracker->GetEntityForBookmarkNode(bookmark), NotNull());
}
// Tests that updates with a UUID that is different to originator client item ID
// are ignored.
TEST(BookmarkModelMergerTest, ShouldIgnoreRemoteUpdateWithInvalidUuid) {
const base::Uuid kUuid1 = base::Uuid::GenerateRandomV4();
const base::Uuid kUuid2 = base::Uuid::GenerateRandomV4();
const std::u16string kTitle1 = u"Title1";
const std::u16string kTitle2 = u"Title2";
const std::u16string kLocalTitle = u"LocalTitle";
const std::string kUrl = "http://www.foo.com/";
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
const base::Uuid kUnexpectedOriginatorItemId = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
// | - bookmark(kUuid/kUrl/kLocalTitle)
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* bookmark = bookmark_model.AddURL(
/*parent=*/bookmark_bar_node, /*index=*/0, kLocalTitle, GURL(kUrl),
/*meta_info=*/nullptr, base::Time::Now(), kUuid);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// | - bookmark (kUuid/kUrl/kTitle1)
// | - bookmark (kUuid/kUrl/kTitle2)
const syncer::UniquePosition::Suffix suffix =
syncer::UniquePosition::RandomSuffix();
syncer::UniquePosition position1 =
syncer::UniquePosition::InitialPosition(suffix);
syncer::UniquePosition position2 =
syncer::UniquePosition::After(position1, suffix);
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle1),
/*url=*/kUrl,
/*is_folder=*/false, /*unique_position=*/position1));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle2),
/*url=*/kUrl,
/*is_folder=*/false, /*unique_position=*/position2));
// |originator_client_item_id| cannot itself be duplicated because
// DataTypeWorker guarantees otherwise.
updates.back().entity.originator_client_item_id =
kUnexpectedOriginatorItemId.AsLowercaseString();
updates.back().entity.id =
GetFakeServerIdFromUuid(kUnexpectedOriginatorItemId);
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
// -------- The merged model --------
// | - bookmark (kUuid/kUrl/kTitle1)
// The second remote node should have been filtered out.
ASSERT_EQ(bookmark_bar_node->children().size(), 1u);
const bookmarks::BookmarkNode* merged_bookmark =
bookmark_model.bookmark_bar_node()->children()[0].get();
EXPECT_THAT(merged_bookmark->uuid(), Eq(kUuid));
EXPECT_THAT(tracker->GetEntityForBookmarkNode(merged_bookmark), NotNull());
}
// Regression test for crbug.com/1050776. Verifies that computing the unique
// position does not crash when processing local creation of bookmark during
// initial merge.
TEST(BookmarkModelMergerTest,
ShouldProcessLocalCreationWithUntrackedPredecessorNode) {
const std::u16string kFolder1Title = u"folder1";
const std::u16string kFolder2Title = u"folder2";
const std::u16string kUrl1Title = u"url1";
const std::u16string kUrl2Title = u"url2";
const std::string kUrl1 = "http://www.url1.com/";
const std::string kUrl2 = "http://www.url2.com/";
const base::Uuid kFolder1Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kFolder2Uuid = base::Uuid::GenerateRandomV4();
const std::string kUrl1Id = "Url1Id";
// It is needed to use at least two folders to reproduce the crash. It is
// needed because the bookmarks are processed in the order of remote entities
// on the same level of the tree. To start processing of locally created
// bookmarks while other remote bookmarks are not processed we need to use at
// least one local folder with several urls.
//
// -------- The local model --------
// bookmark_bar
// |- folder 1
// |- url1(http://www.url1.com)
// |- url2(http://www.url2.com)
TestBookmarkModelView bookmark_model;
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* folder1 = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0, kFolder1Title);
const bookmarks::BookmarkNode* folder1_url1_node = bookmark_model.AddURL(
/*parent=*/folder1, /*index=*/0, kUrl1Title, GURL(kUrl1));
bookmark_model.AddURL(
/*parent=*/folder1, /*index=*/1, kUrl2Title, GURL(kUrl2));
// The remote model contains two folders. The first one is the same as in
// local model, but it does not contain any urls. The second one has the url1
// from first folder with same UUID. This will cause skip local creation for
// |url1| while processing |folder1|.
//
// -------- The remote model --------
// bookmark_bar
// |- folder 1
// |- folder 2
// |- url1(http://www.url1.com)
const syncer::UniquePosition::Suffix suffix =
syncer::UniquePosition::RandomSuffix();
syncer::UniquePosition posFolder1 =
syncer::UniquePosition::InitialPosition(suffix);
syncer::UniquePosition posFolder2 =
syncer::UniquePosition::After(posFolder1, suffix);
syncer::UniquePosition posUrl1 =
syncer::UniquePosition::InitialPosition(suffix);
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kFolder1Uuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kFolder1Title),
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder1));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kFolder2Uuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kFolder2Title),
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder2));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/folder1_url1_node->uuid(), /*parent_uuid=*/kFolder2Uuid,
base::UTF16ToUTF8(kUrl1Title), kUrl1,
/*is_folder=*/false, /*unique_position=*/posUrl1));
// -------- The expected merge outcome --------
// bookmark_bar
// |- folder 1
// |- url2(http://www.url2.com)
// |- folder 2
// |- url1(http://www.url1.com)
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
ASSERT_THAT(bookmark_bar_node->children().size(), Eq(2u));
// Verify Folder 1.
EXPECT_THAT(bookmark_bar_node->children()[0]->GetTitle(), Eq(kFolder1Title));
ASSERT_THAT(bookmark_bar_node->children()[0]->children().size(), Eq(1u));
EXPECT_THAT(bookmark_bar_node->children()[0]->children()[0]->GetTitle(),
Eq(kUrl2Title));
EXPECT_THAT(bookmark_bar_node->children()[0]->children()[0]->url(),
Eq(GURL(kUrl2)));
// Verify Folder 2.
EXPECT_THAT(bookmark_bar_node->children()[1]->GetTitle(), Eq(kFolder2Title));
ASSERT_THAT(bookmark_bar_node->children()[1]->children().size(), Eq(1u));
EXPECT_THAT(bookmark_bar_node->children()[1]->children()[0]->GetTitle(),
Eq(kUrl1Title));
EXPECT_THAT(bookmark_bar_node->children()[1]->children()[0]->url(),
Eq(GURL(kUrl1)));
// Verify the tracker contents.
EXPECT_THAT(tracker->TrackedEntitiesCountForTest(), Eq(5U));
std::vector<const SyncedBookmarkTrackerEntity*> local_changes =
tracker->GetEntitiesWithLocalChanges();
ASSERT_THAT(local_changes.size(), Eq(1U));
EXPECT_THAT(local_changes[0]->bookmark_node(),
Eq(bookmark_bar_node->children()[0]->children()[0].get()));
// Verify positions in tracker.
EXPECT_TRUE(PositionsInTrackerMatchModel(bookmark_bar_node, *tracker));
}
TEST(BookmarkModelMergerTest, ShouldLogMetricsForInvalidSpecifics) {
TestBookmarkModelView bookmark_model;
// -------- The remote model --------
// bookmark_bar
// | - bookmark (<invalid url>)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/base::Uuid::GenerateRandomV4(),
/*parent_uuid=*/BookmarkBarUuid(), "Title",
/*url=*/"invalidurl",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
base::HistogramTester histogram_tester;
Merge(std::move(updates), &bookmark_model);
histogram_tester.ExpectUniqueSample(
"Sync.ProblematicServerSideBookmarksDuringMerge",
/*sample=*/ExpectedRemoteBookmarkUpdateError::kInvalidSpecifics,
/*expected_bucket_count=*/1);
}
TEST(BookmarkModelMergerTest, ShouldLogMetricsForChildrenOfNonFolder) {
TestBookmarkModelView bookmark_model;
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
// -------- The remote model --------
// bookmark_bar
// | - bookmark (url1/Title1)
// | - bookmark (url2/Title2)
// | - bookmark (url3/Title3)
// | - bookmark (url4/Title4)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(), "Title1",
/*url=*/"http://url1",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/base::Uuid::GenerateRandomV4(), /*parent_uuid=*/kUuid, "Title2",
/*url=*/"http://url2",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/base::Uuid::GenerateRandomV4(), /*parent_uuid=*/kUuid, "Title3",
/*url=*/"http://url3",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/base::Uuid::GenerateRandomV4(), /*parent_uuid=*/kUuid, "Title4",
/*url=*/"http://url4",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
base::HistogramTester histogram_tester;
Merge(std::move(updates), &bookmark_model);
histogram_tester.ExpectUniqueSample(
"Sync.ProblematicServerSideBookmarksDuringMerge",
/*sample=*/ExpectedRemoteBookmarkUpdateError::kParentNotFolder,
/*expected_bucket_count=*/3);
}
TEST(BookmarkModelMergerTest, ShouldLogMetricsForChildrenOfOrphanUpdates) {
TestBookmarkModelView bookmark_model;
// -------- The remote model --------
// bookmark_bar
// Orphan node: bookmark(url1/title1)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/base::Uuid::GenerateRandomV4(),
/*parent_uuid=*/base::Uuid::GenerateRandomV4(), "Title1",
/*url=*/"http://url1",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
base::HistogramTester histogram_tester;
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
ASSERT_THAT(tracker, NotNull());
EXPECT_THAT(histogram_tester.GetTotalSum(
"Sync.BookmarkModelMerger.ValidInputUpdates"),
Eq(2));
histogram_tester.ExpectUniqueSample(
"Sync.ProblematicServerSideBookmarksDuringMerge",
/*sample=*/ExpectedRemoteBookmarkUpdateError::kMissingParentEntity,
/*expected_bucket_count=*/1);
EXPECT_THAT(histogram_tester.GetTotalSum(
"Sync.BookmarkModelMerger.ReachableInputUpdates"),
Eq(1));
EXPECT_THAT(tracker->GetNumIgnoredUpdatesDueToMissingParentForTest(), Eq(1));
}
TEST(BookmarkModelMergerTest, ShouldLogMetricsForUnsupportedServerTag) {
TestBookmarkModelView bookmark_model;
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.back().entity.server_defined_unique_tag = "someunknowntag";
base::HistogramTester histogram_tester;
Merge(std::move(updates), &bookmark_model);
histogram_tester.ExpectUniqueSample(
"Sync.ProblematicServerSideBookmarksDuringMerge",
/*sample=*/ExpectedRemoteBookmarkUpdateError::kUnsupportedPermanentFolder,
/*expected_bucket_count=*/1);
}
TEST(BookmarkModelMergerTest, ShouldLogMetricsForkDescendantOfRootNode) {
const std::string kRootNodeId = "test_root_node_id";
TestBookmarkModelView bookmark_model;
// -------- The remote model --------
// root node
// | - bookmark (url1/Title1)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.back().entity.id = kRootNodeId;
updates.back().entity.server_defined_unique_tag =
syncer::DataTypeToProtocolRootTag(syncer::BOOKMARKS);
updates.push_back(CreateUpdateResponseData(
/*uuid=*/base::Uuid::GenerateRandomV4(),
base::Uuid::ParseLowercase(bookmarks::kRootNodeUuid), "Title1",
/*url=*/"http://url1",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
base::HistogramTester histogram_tester;
Merge(std::move(updates), &bookmark_model);
histogram_tester.ExpectUniqueSample(
"Sync.ProblematicServerSideBookmarksDuringMerge",
/*sample=*/ExpectedRemoteBookmarkUpdateError::kMissingParentEntity,
/*expected_bucket_count=*/1);
}
TEST(BookmarkModelMergerTest, ShouldRemoveMatchingDuplicatesByUuid) {
const std::u16string kTitle1 = u"Title 1";
const std::u16string kTitle2 = u"Title 2";
const std::u16string kTitle3 = u"Title 3";
const std::string kUrl = "http://www.url.com/";
const base::Uuid kUrlUuid = base::Uuid::GenerateRandomV4();
// The remote model has 2 duplicate folders with the same title and 2
// duplicate bookmarks with the same URL.
//
// -------- The remote model --------
// bookmark_bar
// |- url1(http://www.url.com, UrlUuid)
// |- url2(http://www.url.com, UrlUuid)
// |- url3(http://www.url.com, <other-uuid>)
TestBookmarkModelView bookmark_model;
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUrlUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle1),
/*url=*/kUrl,
/*is_folder=*/false, /*unique_position=*/MakeRandomPosition()));
updates.back().entity.id = "Id1";
updates.back().entity.creation_time = base::Time::Now() - base::Days(1);
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUrlUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle2),
/*url=*/kUrl,
/*is_folder=*/false, /*unique_position=*/MakeRandomPosition()));
updates.back().entity.id = "Id2";
updates.back().entity.creation_time = base::Time::Now();
updates.push_back(CreateUpdateResponseData(
/*uuid=*/base::Uuid::GenerateRandomV4(),
/*parent_uuid=*/BookmarkBarUuid(), base::UTF16ToUTF8(kTitle3),
/*url=*/kUrl,
/*is_folder=*/false, /*unique_position=*/MakeRandomPosition()));
updates.back().entity.id = "Id3";
updates.back().entity.creation_time = base::Time::Now();
base::HistogramTester histogram_tester;
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
EXPECT_THAT(bookmark_bar_node->children(),
UnorderedElementsAre(IsUrlBookmark(kTitle2, kUrl),
IsUrlBookmark(kTitle3, kUrl)));
EXPECT_THAT(histogram_tester.GetTotalSum(
"Sync.BookmarkModelMerger.ValidInputUpdates"),
Eq(4));
histogram_tester.ExpectBucketCount(
"Sync.BookmarksGUIDDuplicates",
/*sample=*/ExpectedBookmarksUuidDuplicates::kMatchingUrls,
/*expected_count=*/1);
EXPECT_THAT(histogram_tester.GetTotalSum(
"Sync.BookmarkModelMerger.ReachableInputUpdates"),
Eq(3));
}
TEST(BookmarkModelMergerTest, ShouldRemoveDifferentDuplicatesByUuid) {
const std::u16string kTitle1 = u"Title 1";
const std::u16string kTitle2 = u"Title 2";
const std::string kUrl = "http://www.url.com/";
const std::string kDifferentUrl = "http://www.different-url.com/";
const base::Uuid kUrlUuid = base::Uuid::GenerateRandomV4();
// The remote model will have 2 duplicate folders with
// different titles and 2 duplicate bookmarks with different URLs
//
// -------- The remote model --------
// bookmark_bar
// |- url1(http://www.url.com, UrlUUID)
// |- url2(http://www.different-url.com, UrlUUID)
TestBookmarkModelView bookmark_model;
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUrlUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle1),
/*url=*/kUrl,
/*is_folder=*/false, /*unique_position=*/MakeRandomPosition()));
updates.back().entity.id = "Id1";
updates.back().entity.creation_time = base::Time::Now();
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUrlUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle2),
/*url=*/kDifferentUrl,
/*is_folder=*/false, /*unique_position=*/MakeRandomPosition()));
updates.back().entity.id = "Id2";
updates.back().entity.creation_time = base::Time::Now() - base::Days(1);
base::HistogramTester histogram_tester;
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
EXPECT_THAT(bookmark_bar_node->children(),
UnorderedElementsAre(IsUrlBookmark(kTitle1, kUrl)));
histogram_tester.ExpectBucketCount(
"Sync.BookmarksGUIDDuplicates",
/*sample=*/ExpectedBookmarksUuidDuplicates::kDifferentUrls,
/*expected_count=*/1);
}
TEST(BookmarkModelMergerTest, ShouldRemoveMatchingFolderDuplicatesByUuid) {
const std::u16string kTitle = u"Title";
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
// The remote model has 2 duplicate folders with the same title and 2
// duplicate bookmarks with the same URL.
//
// -------- The remote model --------
// bookmark_bar
// |- folder1(Title, UUID)
// |- folder2(Title, UUID)
TestBookmarkModelView bookmark_model;
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle),
/*url=*/"",
/*is_folder=*/true, /*unique_position=*/MakeRandomPosition()));
updates.back().entity.id = "Id1";
updates.back().entity.creation_time = base::Time::Now() - base::Days(1);
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle),
/*url=*/"",
/*is_folder=*/true, /*unique_position=*/MakeRandomPosition()));
updates.back().entity.id = "Id2";
updates.back().entity.creation_time = base::Time::Now();
base::HistogramTester histogram_tester;
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
ASSERT_THAT(bookmark_bar_node->children().size(), Eq(1u));
histogram_tester.ExpectBucketCount(
"Sync.BookmarksGUIDDuplicates",
/*sample=*/ExpectedBookmarksUuidDuplicates::kMatchingFolders,
/*expected_count=*/1);
EXPECT_THAT(tracker->GetEntityForSyncId("Id1"), IsNull());
EXPECT_THAT(tracker->GetEntityForSyncId("Id2"), NotNull());
}
TEST(BookmarkModelMergerTest, ShouldRemoveDifferentFolderDuplicatesByUuid) {
const std::u16string kTitle1 = u"Title 1";
const std::u16string kTitle2 = u"Title 2";
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
// The remote model has 2 duplicate folders with the same title and 2
// duplicate bookmarks with the same URL.
//
// -------- The remote model --------
// bookmark_bar
// |- folder1(Title, UUID)
// |- folder11
// |- folder2(Title, UUID)
// |- folder21
TestBookmarkModelView bookmark_model;
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle1),
/*url=*/"",
/*is_folder=*/true, MakeRandomPosition()));
updates.back().entity.id = "Id1";
updates.back().entity.creation_time = base::Time::Now();
updates.push_back(CreateUpdateResponseData(
/*uuid=*/base::Uuid::GenerateRandomV4(), /*parent_uuid=*/kUuid,
"Some title",
/*url=*/"", /*is_folder=*/true, MakeRandomPosition()));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle2),
/*url=*/"", /*is_folder=*/true, MakeRandomPosition()));
updates.back().entity.id = "Id2";
updates.back().entity.creation_time = base::Time::Now() - base::Days(1);
updates.push_back(CreateUpdateResponseData(
/*uuid=*/base::Uuid::GenerateRandomV4(), /*parent_uuid=*/kUuid,
"Some title 2",
/*url=*/"", /*is_folder=*/true, MakeRandomPosition()));
base::HistogramTester histogram_tester;
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
ASSERT_THAT(bookmark_bar_node->children().size(), Eq(1u));
histogram_tester.ExpectBucketCount(
"Sync.BookmarksGUIDDuplicates",
/*sample=*/ExpectedBookmarksUuidDuplicates::kDifferentFolders,
/*expected_count=*/1);
EXPECT_THAT(tracker->GetEntityForSyncId("Id1"), NotNull());
EXPECT_THAT(tracker->GetEntityForSyncId("Id2"), IsNull());
EXPECT_EQ(bookmark_bar_node->children().front()->GetTitle(), kTitle1);
EXPECT_EQ(bookmark_bar_node->children().front()->children().size(), 2u);
}
// This tests ensures maximum depth of the bookmark tree is not exceeded. This
// prevents a stack overflow.
TEST(BookmarkModelMergerTest, ShouldEnsureLimitDepthOfTree) {
const std::u16string kLocalTitle = u"local";
const std::u16string kRemoteTitle = u"remote";
const std::string folderIdPrefix = "folder_";
// Maximum depth to sync bookmarks tree to protect against stack overflow.
// This matches |kMaxBookmarkTreeDepth| in bookmark_model_merger.cc.
const size_t kMaxBookmarkTreeDepth = 200;
const size_t kRemoteUpdatesDepth = kMaxBookmarkTreeDepth + 10;
TestBookmarkModelView bookmark_model;
// -------- The local model --------
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
const bookmarks::BookmarkNode* folder = bookmark_model.AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0, kLocalTitle);
ASSERT_TRUE(folder);
// -------- The remote model --------
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
base::Uuid parent_uuid = BookmarkBarUuid();
// Create a tree with depth |kRemoteUpdatesDepth| to verify the limit of
// kMaxBookmarkTreeDepth is enforced.
for (size_t i = 1; i < kRemoteUpdatesDepth; ++i) {
base::Uuid folder_uuid = base::Uuid::GenerateRandomV4();
updates.push_back(CreateUpdateResponseData(
/*uuid=*/folder_uuid, /*parent_uuid=*/parent_uuid,
base::UTF16ToUTF8(kRemoteTitle),
/*url=*/"",
/*is_folder=*/true, MakeRandomPosition()));
parent_uuid = folder_uuid;
}
ASSERT_THAT(updates.size(), Eq(kRemoteUpdatesDepth));
std::unique_ptr<SyncedBookmarkTracker> tracker =
SyncedBookmarkTracker::CreateEmpty(sync_pb::DataTypeState());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
BookmarkModelMerger(std::move(updates), &bookmark_model, &favicon_service,
tracker.get())
.Merge();
// Check max depth hasn't been exceeded. Take into account root of the
// tracker and bookmark bar.
EXPECT_THAT(tracker->TrackedEntitiesCountForTest(),
Eq(kMaxBookmarkTreeDepth + 2));
}
TEST(BookmarkModelMergerTest, ShouldReuploadBookmarkOnEmptyUniquePosition) {
base::test::ScopedFeatureList override_features{
switches::kSyncReuploadBookmarks};
const std::u16string kFolder1Title = u"folder1";
const std::u16string kFolder2Title = u"folder2";
const base::Uuid kFolder1Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kFolder2Uuid = base::Uuid::GenerateRandomV4();
const syncer::UniquePosition::Suffix suffix =
syncer::UniquePosition::RandomSuffix();
const syncer::UniquePosition posFolder1 =
syncer::UniquePosition::InitialPosition(suffix);
const syncer::UniquePosition posFolder2 =
syncer::UniquePosition::After(posFolder1, suffix);
TestBookmarkModelView bookmark_model;
// -------- The remote model --------
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kFolder1Uuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kFolder1Title),
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder1));
// Mimic that the entity didn't have |unique_position| in specifics. This
// entity should be reuploaded later.
updates.back().entity.is_bookmark_unique_position_in_specifics_preprocessed =
true;
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kFolder2Uuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kFolder2Title),
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder2));
base::HistogramTester histogram_tester;
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
ASSERT_THAT(tracker->GetEntityForUuid(kFolder1Uuid), NotNull());
ASSERT_THAT(tracker->GetEntityForUuid(kFolder2Uuid), NotNull());
EXPECT_TRUE(tracker->GetEntityForUuid(kFolder1Uuid)->IsUnsynced());
EXPECT_FALSE(tracker->GetEntityForUuid(kFolder2Uuid)->IsUnsynced());
EXPECT_THAT(histogram_tester.GetTotalSum(
"Sync.BookmarkModelMerger.UnsyncedEntitiesUponCompletion"),
Eq(1));
}
TEST(BookmarkModelMergerTest, ShouldRemoveDifferentTypeDuplicatesByUuid) {
const std::u16string kTitle = u"Title";
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
// The remote model has 2 duplicates, a folder and a URL.
//
// -------- The remote model --------
// bookmark_bar
// |- folder1(UUID)
// |- folder11
// |- URL1(UUID)
TestBookmarkModelView bookmark_model;
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle),
/*url=*/"",
/*is_folder=*/true, MakeRandomPosition()));
updates.back().entity.id = "Id1";
updates.push_back(CreateUpdateResponseData(
/*uuid=*/base::Uuid::GenerateRandomV4(), /*parent_uuid=*/kUuid,
"Some title",
/*url=*/"", /*is_folder=*/true, MakeRandomPosition()));
updates.push_back(CreateUpdateResponseData(
/*uuid=*/kUuid, /*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kTitle),
/*url=*/"http://url1.com", /*is_folder=*/false, MakeRandomPosition()));
updates.back().entity.id = "Id2";
base::HistogramTester histogram_tester;
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
ASSERT_THAT(bookmark_bar_node->children().size(), Eq(1u));
histogram_tester.ExpectUniqueSample(
"Sync.BookmarksGUIDDuplicates",
/*sample=*/ExpectedBookmarksUuidDuplicates::kDifferentTypes,
/*expected_bucket_count=*/1);
EXPECT_THAT(tracker->GetEntityForSyncId("Id1"), NotNull());
EXPECT_THAT(tracker->GetEntityForSyncId("Id2"), IsNull());
EXPECT_EQ(bookmark_bar_node->children().front()->children().size(), 1u);
}
TEST(BookmarkModelMergerTest, ShouldReportTimeMetrics) {
const std::u16string kTitle = u"Title";
TestBookmarkModelView bookmark_model;
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
// Create 10k+ bookmarks to verify reported metrics.
for (size_t i = 0; i < 10001; ++i) {
updates.push_back(CreateUpdateResponseData(
/*uuid=*/base::Uuid::GenerateRandomV4(),
/*parent_uuid=*/BookmarkBarUuid(), base::UTF16ToUTF8(kTitle),
/*url=*/"",
/*is_folder=*/true, MakeRandomPosition()));
}
base::HistogramTester histogram_tester;
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
histogram_tester.ExpectTotalCount("Sync.BookmarkModelMergerTime", 1);
histogram_tester.ExpectTotalCount("Sync.BookmarkModelMergerTime.10kUpdates",
1);
histogram_tester.ExpectTotalCount("Sync.BookmarkModelMergerTime.50kUpdates",
0);
histogram_tester.ExpectTotalCount("Sync.BookmarkModelMergerTime.100kUpdates",
0);
}
TEST(BookmarkModelMergerTest, ShouldMigrateBookmarksWithoutClientTagHash) {
base::test::ScopedFeatureList override_features{
switches::kSyncMigrateBookmarksWithoutClientTagHash};
const std::u16string kFolder1Title = u"folder1";
const std::u16string kFolder2Title = u"folder2";
const std::u16string kUrl1Title = u"url1";
const std::u16string kUrl2Title = u"url2";
const std::u16string kUrl3Title = u"url3";
const std::u16string kUrl4Title = u"url4";
const GURL kUrl1("http://www.url1.com");
const GURL kUrl2("http://www.url2.com");
const GURL kUrl3("http://www.url3.com");
const GURL kUrl4("http://www.url4.com");
const base::Uuid kFolder1Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kFolder2Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kUrl1Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kUrl2Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kUrl3Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kUrl4Uuid = base::Uuid::GenerateRandomV4();
TestBookmarkModelView bookmark_model;
// -------- The local model --------
// bookmark_bar
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model.bookmark_bar_node();
ASSERT_THAT(bookmark_bar_node->children(), IsEmpty());
// -------- The remote model --------
// bookmark_bar
// |- folder 1 (kFolder1Uuid), no client tag hash
// |- kUrl1 (kUrl1Uuid), no client tag hash
// |- kUrl2 (kUrl2Uuid), with client tag hash
// |- folder 2 (kFolder2Uuid), with client tag hash
// |- kUrl3 (kUrl3Uuid), no client tag hash
// |- kUrl4 (kUrl4Uuid), with client tag hash
const syncer::UniquePosition::Suffix suffix =
syncer::UniquePosition::RandomSuffix();
syncer::UniquePosition posFolder1 =
syncer::UniquePosition::InitialPosition(suffix);
syncer::UniquePosition posFolder2 =
syncer::UniquePosition::After(posFolder1, suffix);
syncer::UniquePosition posUrl1 =
syncer::UniquePosition::InitialPosition(suffix);
syncer::UniquePosition posUrl2 =
syncer::UniquePosition::After(posUrl1, suffix);
syncer::UniquePosition posUrl3 =
syncer::UniquePosition::InitialPosition(suffix);
syncer::UniquePosition posUrl4 =
syncer::UniquePosition::After(posUrl3, suffix);
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(UpdateResponseDataBuilder(
/*uuid=*/kFolder1Uuid,
/*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kFolder1Title),
/*unique_position=*/posFolder1)
.Build());
const std::string kFolder1SyncId = updates.back().entity.id;
updates.push_back(UpdateResponseDataBuilder(
/*uuid=*/kUrl1Uuid, /*parent_uuid=*/kFolder1Uuid,
base::UTF16ToUTF8(kUrl1Title),
/*unique_position=*/posUrl1)
.SetUrl(kUrl1)
.Build());
const std::string kUrl1SyncId = updates.back().entity.id;
updates.push_back(UpdateResponseDataBuilder(
/*uuid=*/kUrl2Uuid, /*parent_uuid=*/kFolder1Uuid,
base::UTF16ToUTF8(kUrl2Title),
/*unique_position=*/posUrl2)
.WithClientTagHash()
.SetUrl(kUrl2)
.Build());
updates.push_back(UpdateResponseDataBuilder(
/*uuid=*/kFolder2Uuid,
/*parent_uuid=*/BookmarkBarUuid(),
base::UTF16ToUTF8(kFolder2Title),
/*unique_position=*/posFolder2)
.WithClientTagHash()
.Build());
updates.push_back(UpdateResponseDataBuilder(
/*uuid=*/kUrl3Uuid, /*parent_uuid=*/kFolder2Uuid,
base::UTF16ToUTF8(kUrl3Title),
/*unique_position=*/posUrl3)
.SetUrl(kUrl3)
.Build());
const std::string kUrl3SyncId = updates.back().entity.id;
updates.push_back(UpdateResponseDataBuilder(
/*uuid=*/kUrl4Uuid, /*parent_uuid=*/kFolder2Uuid,
base::UTF16ToUTF8(kUrl4Title),
/*unique_position=*/posUrl4)
.SetUrl(kUrl4)
.WithClientTagHash()
.Build());
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), &bookmark_model);
// -------- The merged model --------
// bookmark_bar
// |- folder 1 ([new UUID])
// |- kUrl1 ([new UUID])
// |- kUrl2 (kUrl2Uuid)
// |- folder 2 (kFolder2Uuid)
// |- kUrl3 ([new UUID])
// |- kUrl4 (kUrl4Uuid)
//
// The conflicting node UUID should have been replaced.
EXPECT_THAT(
bookmark_bar_node->children(),
ElementsAre(
IsFolderWithUuid(
kFolder1Title, Ne(kFolder1Uuid),
ElementsAre(
IsUrlBookmarkWithUuid(kUrl1Title, kUrl1, Ne(kUrl1Uuid)),
IsUrlBookmarkWithUuid(kUrl2Title, kUrl2, kUrl2Uuid))),
IsFolderWithUuid(
kFolder2Title, kFolder2Uuid,
ElementsAre(
IsUrlBookmarkWithUuid(kUrl3Title, kUrl3, Ne(kUrl3Uuid)),
IsUrlBookmarkWithUuid(kUrl4Title, kUrl4, kUrl4Uuid)))));
// Three bookmarks got migrated via creation+deletion and one more (kUrl2) is
// expected to be unsynced because the parent changed.
EXPECT_THAT(tracker->GetEntitiesWithLocalChanges(),
UnorderedElementsAre(
IsUnsyncedBookmarkEntity(IsFolder(kFolder1Title, _)),
IsUnsyncedBookmarkEntity(IsUrlBookmark(kUrl1Title, kUrl1)),
IsUnsyncedBookmarkEntity(IsUrlBookmark(kUrl2Title, kUrl2)),
IsUnsyncedBookmarkEntity(IsUrlBookmark(kUrl3Title, kUrl3)),
IsTombstone(kFolder1SyncId), IsTombstone(kUrl1SyncId),
IsTombstone(kUrl3SyncId)));
}
} // namespace sync_bookmarks