blob: 46f24c3c9649425e11e784d9e31156c72d3a7801 [file] [log] [blame]
// 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_model_merger.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/guid.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 "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/test/test_bookmark_client.h"
#include "components/favicon/core/test/mock_favicon_service.h"
#include "components/sync/base/unique_position.h"
#include "components/sync/protocol/entity_metadata.pb.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 "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using testing::_;
using testing::Eq;
using testing::IsNull;
using testing::Ne;
using testing::NotNull;
using testing::UnorderedElementsAre;
namespace sync_bookmarks {
namespace {
MATCHER_P(HasTitle, title, "") {
return arg->GetTitle() == title;
}
// Copy of BookmarksGUIDDuplicates.
enum class ExpectedBookmarksGUIDDuplicates {
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,
kUnexpectedGuid = 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::GUID BookmarkBarGuid() {
return base::GUID::ParseLowercase(
bookmarks::BookmarkNode::kBookmarkBarNodeGuid);
}
// 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;
}
return base::StrCat({"server_id_for_", guid.AsLowercaseString()});
}
class UpdateResponseDataBuilder {
public:
UpdateResponseDataBuilder(const base::GUID& guid,
const base::GUID& parent_guid,
const std::string& title,
const syncer::UniquePosition& unique_position) {
data_.id = GetFakeServerIdFromGUID(guid);
data_.originator_client_item_id = guid.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(guid.AsLowercaseString());
bookmark_specifics->set_parent_guid(parent_guid.AsLowercaseString());
}
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::GUID& guid,
const base::GUID& parent_guid,
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(guid, parent_guid, 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::all_of(node->children().cbegin(), node->children().cend(),
[&tracker](const auto& child) {
return PositionsInTrackerMatchModel(child.get(),
tracker);
});
}
std::unique_ptr<SyncedBookmarkTracker> Merge(
syncer::UpdateResponseDataList updates,
bookmarks::BookmarkModel* bookmark_model) {
std::unique_ptr<SyncedBookmarkTracker> tracker =
SyncedBookmarkTracker::CreateEmpty(sync_pb::ModelTypeState());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
BookmarkModelMerger(std::move(updates), bookmark_model, &favicon_service,
tracker.get())
.Merge();
return tracker;
}
static syncer::UniquePosition MakeRandomPosition() {
const std::string suffix = syncer::UniquePosition::RandomSuffix();
return syncer::UniquePosition::InitialPosition(suffix);
}
} // namespace
TEST(BookmarkModelMergerTest, ShouldMergeLocalAndRemoteModels) {
const std::string kFolder1Title = "folder1";
const std::string kFolder2Title = "folder2";
const std::string kFolder3Title = "folder3";
const std::string kUrl1Title = "url1";
const std::string kUrl2Title = "url2";
const std::string kUrl3Title = "url3";
const std::string kUrl4Title = "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::GUID kFolder1Guid = base::GUID::GenerateRandomV4();
const base::GUID kFolder3Guid = base::GUID::GenerateRandomV4();
const base::GUID kUrl1Guid = base::GUID::GenerateRandomV4();
const base::GUID kUrl2Guid = base::GUID::GenerateRandomV4();
const base::GUID kUrl3Guid = base::GUID::GenerateRandomV4();
const base::GUID kUrl4Guid = base::GUID::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)
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model->bookmark_bar_node();
const bookmarks::BookmarkNode* folder1 = bookmark_model->AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0,
base::UTF8ToUTF16(kFolder1Title));
const bookmarks::BookmarkNode* folder2 = bookmark_model->AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/1,
base::UTF8ToUTF16(kFolder2Title));
bookmark_model->AddURL(
/*parent=*/folder1, /*index=*/0, base::UTF8ToUTF16(kUrl1Title),
GURL(kUrl1));
bookmark_model->AddURL(
/*parent=*/folder1, /*index=*/1, base::UTF8ToUTF16(kUrl2Title),
GURL(kUrl2));
bookmark_model->AddURL(
/*parent=*/folder2, /*index=*/0, base::UTF8ToUTF16(kUrl3Title),
GURL(kUrl3));
bookmark_model->AddURL(
/*parent=*/folder2, /*index=*/1, base::UTF8ToUTF16(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 std::string 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(
/*guid=*/kFolder1Guid, /*parent_guid=*/BookmarkBarGuid(), kFolder1Title,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder1));
updates.push_back(CreateUpdateResponseData(
/*guid=*/kUrl1Guid, /*parent_guid=*/kFolder1Guid, kUrl1Title, kUrl1,
/*is_folder=*/false, /*unique_position=*/posUrl1));
updates.push_back(CreateUpdateResponseData(
/*guid=*/kUrl2Guid, /*parent_guid=*/kFolder1Guid, kUrl2Title,
kAnotherUrl2,
/*is_folder=*/false, /*unique_position=*/posUrl2));
updates.push_back(CreateUpdateResponseData(
/*guid=*/kFolder3Guid, /*parent_guid=*/BookmarkBarGuid(), kFolder3Title,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder3));
updates.push_back(CreateUpdateResponseData(
/*guid=*/kUrl3Guid, /*parent_guid=*/kFolder3Guid, kUrl3Title, kUrl3,
/*is_folder=*/false, /*unique_position=*/posUrl3));
updates.push_back(CreateUpdateResponseData(
/*guid=*/kUrl4Guid, /*parent_guid=*/kFolder3Guid, 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.get());
ASSERT_THAT(bookmark_bar_node->children().size(), Eq(3u));
// Verify Folder 1.
EXPECT_THAT(bookmark_bar_node->children()[0]->GetTitle(),
Eq(base::ASCIIToUTF16(kFolder1Title)));
ASSERT_THAT(bookmark_bar_node->children()[0]->children().size(), Eq(3u));
EXPECT_THAT(bookmark_bar_node->children()[0]->children()[0]->GetTitle(),
Eq(base::ASCIIToUTF16(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(base::ASCIIToUTF16(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(base::ASCIIToUTF16(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(base::ASCIIToUTF16(kFolder3Title)));
ASSERT_THAT(bookmark_bar_node->children()[1]->children().size(), Eq(2u));
EXPECT_THAT(bookmark_bar_node->children()[1]->children()[0]->GetTitle(),
Eq(base::ASCIIToUTF16(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(base::ASCIIToUTF16(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(base::ASCIIToUTF16(kFolder2Title)));
ASSERT_THAT(bookmark_bar_node->children()[2]->children().size(), Eq(2u));
EXPECT_THAT(bookmark_bar_node->children()[2]->children()[0]->GetTitle(),
Eq(base::ASCIIToUTF16(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(base::ASCIIToUTF16(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::string kFolder1Title = "folder1";
const std::string kFolder2Title = "folder2";
const std::string kFolder3Title = "folder3";
const base::GUID kFolder1Guid = base::GUID::GenerateRandomV4();
const base::GUID kFolder2Guid = base::GUID::GenerateRandomV4();
const base::GUID kFolder3Guid = base::GUID::GenerateRandomV4();
// -------- The local model --------
// bookmark_bar
// |- folder 1
// |- folder 2
// |- folder 3
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model->bookmark_bar_node();
bookmark_model->AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0,
base::UTF8ToUTF16(kFolder1Title));
bookmark_model->AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/1,
base::UTF8ToUTF16(kFolder2Title));
bookmark_model->AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/2,
base::UTF8ToUTF16(kFolder3Title));
// -------- The remote model --------
// bookmark_bar
// |- folder 1
// |- folder 3
// |- folder 2
const std::string 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(
/*guid=*/kFolder1Guid, /*parent_guid=*/BookmarkBarGuid(), kFolder1Title,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder1));
updates.push_back(CreateUpdateResponseData(
/*guid=*/kFolder2Guid, /*parent_guid=*/BookmarkBarGuid(), kFolder2Title,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder2));
updates.push_back(CreateUpdateResponseData(
/*guid=*/kFolder3Guid, /*parent_guid=*/BookmarkBarGuid(), 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.get());
ASSERT_THAT(bookmark_bar_node->children().size(), Eq(3u));
EXPECT_THAT(bookmark_bar_node->children()[0]->GetTitle(),
Eq(base::ASCIIToUTF16(kFolder1Title)));
EXPECT_THAT(bookmark_bar_node->children()[1]->GetTitle(),
Eq(base::ASCIIToUTF16(kFolder3Title)));
EXPECT_THAT(bookmark_bar_node->children()[2]->GetTitle(),
Eq(base::ASCIIToUTF16(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, ShouldMergeFaviconsForRemoteNodesOnly) {
const std::string kTitle1 = "title1";
const GURL kUrl1("http://www.url1.com");
// -------- The local model --------
// bookmark_bar
// |- title 1
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model->bookmark_bar_node();
bookmark_model->AddURL(
/*parent=*/bookmark_bar_node, /*index=*/0, base::UTF8ToUTF16(kTitle1),
kUrl1);
// -------- The remote model --------
// bookmark_bar
// |- title 2
const std::string kTitle2 = "title2";
const base::GUID kGuid2 = base::GUID::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(
/*guid=*/kGuid2, /*parent_guid=*/BookmarkBarGuid(), 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::ModelTypeState());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
// Favicon should be set for the remote node.
EXPECT_CALL(favicon_service,
AddPageNoVisitForBookmark(kUrl2, base::UTF8ToUTF16(kTitle2)));
EXPECT_CALL(favicon_service, MergeFavicon(kUrl2, _, _, _, _));
BookmarkModelMerger(std::move(updates), bookmark_model.get(),
&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::string kLocalTitle = "";
const std::string kRemoteTitle = " ";
const base::GUID kGuid = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- 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::UTF8ToUTF16(kLocalTitle));
ASSERT_TRUE(folder);
// -------- The remote model --------
const std::string suffix = syncer::UniquePosition::RandomSuffix();
syncer::UniquePosition pos = syncer::UniquePosition::InitialPosition(suffix);
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(UpdateResponseDataBuilder(/*guid=*/kGuid,
/*parent_guid=*/BookmarkBarGuid(),
kRemoteTitle,
/*unique_position=*/pos)
.SetLegacyTitleOnly()
.Build());
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), bookmark_model.get());
// 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::string kLocalLongTitle =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrst"
"uvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN"
"OPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh"
"ijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB"
"CDEFGHIJKLMNOPQRSTUVWXYZ";
const std::string kRemoteTruncatedTitle =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrst"
"uvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN"
"OPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh"
"ijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTU";
const base::GUID kGuid = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- 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::UTF8ToUTF16(kLocalLongTitle));
ASSERT_TRUE(folder);
// -------- The remote model --------
const std::string suffix = syncer::UniquePosition::RandomSuffix();
syncer::UniquePosition pos = syncer::UniquePosition::InitialPosition(suffix);
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), kRemoteTruncatedTitle,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/pos));
std::unique_ptr<SyncedBookmarkTracker> tracker =
SyncedBookmarkTracker::CreateEmpty(sync_pb::ModelTypeState());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
BookmarkModelMerger(std::move(updates), bookmark_model.get(),
&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::string kLocalLongTitle(300, 'A');
const std::string kRemoteTruncatedFullTitle(255, 'A');
const base::GUID kGuid = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- 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::UTF8ToUTF16(kLocalLongTitle));
ASSERT_TRUE(folder);
// -------- The remote model --------
const std::string suffix = syncer::UniquePosition::RandomSuffix();
syncer::UniquePosition pos = syncer::UniquePosition::InitialPosition(suffix);
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(),
kRemoteTruncatedFullTitle,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/pos));
updates.back().entity.specifics.mutable_bookmark()->set_full_title(
kRemoteTruncatedFullTitle);
std::unique_ptr<SyncedBookmarkTracker> tracker =
SyncedBookmarkTracker::CreateEmpty(sync_pb::ModelTypeState());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
BookmarkModelMerger(std::move(updates), bookmark_model.get(),
&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::GUID kGuid = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- 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::UTF8ToUTF16(kLocalTruncatedTitle));
ASSERT_TRUE(folder);
// -------- The remote model --------
const std::string suffix = syncer::UniquePosition::RandomSuffix();
syncer::UniquePosition pos = syncer::UniquePosition::InitialPosition(suffix);
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(),
sync_bookmarks::FullTitleToLegacyCanonicalizedTitle(kRemoteFullTitle),
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/pos));
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::ModelTypeState());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
BookmarkModelMerger(std::move(updates), bookmark_model.get(),
&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, ShouldMergeAndUseRemoteGUID) {
const base::GUID kGuid = base::GUID::GenerateRandomV4();
const std::string kTitle = "Title";
const base::GUID kRemoteGuid = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- 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::UTF8ToUTF16(kTitle));
ASSERT_TRUE(folder);
// -------- The remote model --------
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kRemoteGuid, /*parent_guid=*/BookmarkBarGuid(), kTitle,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/MakeRandomPosition()));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), bookmark_model.get());
// Node should have been replaced and GUID 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->guid(), kRemoteGuid);
EXPECT_THAT(tracker->GetEntityForBookmarkNode(bookmark), NotNull());
}
TEST(BookmarkModelMergerTest,
ShouldMergeAndKeepOldGUIDWhenRemoteGUIDIsInvalid) {
const base::GUID kGuid = base::GUID::GenerateRandomV4();
const std::string kTitle = "Title";
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- 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::UTF8ToUTF16(kTitle));
ASSERT_TRUE(folder);
const base::GUID old_guid = folder->guid();
// -------- The remote model --------
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/base::GUID::GenerateRandomV4(),
/*parent_guid=*/BookmarkBarGuid(), 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.get());
// Node should not have been replaced and GUID 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->guid(), old_guid);
EXPECT_THAT(tracker->GetEntityForBookmarkNode(bookmark), NotNull());
}
TEST(BookmarkModelMergerTest, ShouldMergeBookmarkByGUID) {
const std::string kLocalTitle = "Title 1";
const std::string kRemoteTitle = "Title 2";
const std::string kUrl = "http://www.foo.com/";
const base::GUID kGuid = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- The local model --------
// bookmark_bar
// | - bookmark(kGuid/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, base::UTF8ToUTF16(kLocalTitle),
GURL(kUrl), /*meta_info=*/nullptr, base::Time::Now(), kGuid);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// | - bookmark(kGuid/kRemoteTitle)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), kRemoteTitle,
/*url=*/kUrl,
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), bookmark_model.get());
// -------- The merged model --------
// bookmark_bar
// |- bookmark(kGuid/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, ShouldMergeBookmarkByGUIDAndReparent) {
const std::string kLocalTitle = "Title 1";
const std::string kRemoteTitle = "Title 2";
const std::string kUrl = "http://www.foo.com/";
const base::GUID kGuid = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- The local model --------
// bookmark_bar
// | - folder
// | - bookmark(kGuid)
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, base::UTF8ToUTF16(kLocalTitle),
GURL(kUrl), /*meta_info=*/nullptr, base::Time::Now(), kGuid);
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(kGuid)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), kRemoteTitle,
/*url=*/kUrl,
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), bookmark_model.get());
// -------- The merged model --------
// bookmark_bar
// | - bookmark(kGuid/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, ShouldMergeFolderByGUIDAndNotSemantics) {
const std::string kFolderId = "Folder Id";
const std::string kTitle1 = "Title 1";
const std::string kTitle2 = "Title 2";
const std::string kUrl = "http://www.foo.com/";
const base::GUID kGuid1 = base::GUID::GenerateRandomV4();
const base::GUID kGuid2 = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- The local model --------
// bookmark_bar
// | - folder 1 (kGuid1/kTitle1)
// | - folder 2 (kGuid2/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, base::UTF8ToUTF16(kTitle1),
/*meta_info=*/nullptr, /*creation_time=*/base::Time::Now(), kGuid1);
const bookmarks::BookmarkNode* folder2 = bookmark_model->AddFolder(
/*parent=*/folder1, /*index=*/0, base::UTF8ToUTF16(kTitle2),
/*meta_info=*/nullptr, /*creation_time=*/base::Time::Now(), kGuid2);
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 (kGuid2/kTitle1)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
// Add a remote folder to correspond to the local folder by GUID and
// semantics.
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid2, /*parent_guid=*/BookmarkBarGuid(), kTitle1,
/*url=*/"",
/*is_folder=*/true,
/*unique_position=*/MakeRandomPosition()));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), bookmark_model.get());
// -------- The merged model --------
// bookmark_bar
// | - folder 2 (kGuid2/kTitle1)
// | - folder 1 (kGuid1/kTitle1)
// Node should have been merged with its GUID match.
EXPECT_THAT(bookmark_bar_node->children(),
ElementRawPointersAre(folder2, folder1));
EXPECT_EQ(folder1->guid(), kGuid1);
EXPECT_EQ(folder1->GetTitle(), base::UTF8ToUTF16(kTitle1));
EXPECT_EQ(folder1->children().size(), 0u);
EXPECT_EQ(folder2->guid(), kGuid2);
EXPECT_EQ(folder2->GetTitle(), base::UTF8ToUTF16(kTitle1));
EXPECT_THAT(tracker->GetEntityForBookmarkNode(folder1), NotNull());
EXPECT_THAT(tracker->GetEntityForBookmarkNode(folder2), NotNull());
}
TEST(BookmarkModelMergerTest, ShouldIgnoreChildrenForNonFolderNodes) {
const std::string kChildId = "child_id";
const std::string kParentTitle = "Parent Title";
const std::string kChildTitle = "Child Title";
const base::GUID kGuid1 = base::GUID::GenerateRandomV4();
const base::GUID kGuid2 = base::GUID::GenerateRandomV4();
const std::string kUrl1 = "http://www.foo.com/";
const std::string kUrl2 = "http://www.bar.com/";
// -------- The remote model --------
// bookmark_bar
// | - bookmark (kGuid1/kParentTitle, not a folder)
// | - bookmark
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
const std::string 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(
/*guid=*/kGuid1, /*parent_guid=*/BookmarkBarGuid(), kParentTitle,
/*url=*/kUrl1,
/*is_folder=*/false,
/*unique_position=*/pos1));
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid2, /*parent_guid=*/kGuid1, kChildTitle,
/*url=*/kUrl2,
/*is_folder=*/false,
/*unique_position=*/pos2));
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), bookmark_model.get());
// -------- The merged model --------
// bookmark_bar
// | - bookmark (kGuid1/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]->guid(), kGuid1);
EXPECT_EQ(bookmark_bar_node->children()[0]->GetTitle(),
base::UTF8ToUTF16(kParentTitle));
EXPECT_EQ(bookmark_bar_node->children()[0]->children().size(), 0u);
EXPECT_EQ(tracker->TrackedEntitiesCountForTest(), 2U);
}
TEST(
BookmarkModelMergerTest,
ShouldIgnoreFolderSemanticsMatchAndLaterMatchByGUIDWithSemanticsNodeFirst) {
const std::string kFolderId1 = "Folder Id 1";
const std::string kFolderId2 = "Folder Id 2";
const std::string kOriginalTitle = "Original Title";
const std::string kNewTitle = "New Title";
const base::GUID kGuid1 = base::GUID::GenerateRandomV4();
const base::GUID kGuid2 = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- The local model --------
// bookmark_bar
// | - folder (kGuid1/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,
base::UTF8ToUTF16(kOriginalTitle), /*meta_info=*/nullptr,
/*creation_time=*/base::Time::Now(), kGuid1);
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 (kGuid2/kOriginalTitle)
// | - folder (kGuid1/kNewTitle)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
const std::string 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
// GUID.
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid2, /*parent_guid=*/BookmarkBarGuid(), kOriginalTitle,
/*url=*/"",
/*is_folder=*/true,
/*unique_position=*/pos1));
// Add a remote folder to correspond to the local folder by GUID and not
// semantics.
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid1, /*parent_guid=*/BookmarkBarGuid(), kNewTitle,
/*url=*/"",
/*is_folder=*/true,
/*unique_position=*/pos2));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), bookmark_model.get());
// -------- The merged model --------
// bookmark_bar
// | - folder (kGuid2/kOriginalTitle)
// | - folder (kGuid1/kNewTitle)
// | - bookmark
// Node should have been merged with its GUID match.
ASSERT_EQ(bookmark_bar_node->children().size(), 2u);
EXPECT_EQ(bookmark_bar_node->children()[0]->guid(), kGuid2);
EXPECT_EQ(bookmark_bar_node->children()[0]->GetTitle(),
base::UTF8ToUTF16(kOriginalTitle));
EXPECT_EQ(bookmark_bar_node->children()[0]->children().size(), 0u);
EXPECT_EQ(bookmark_bar_node->children()[1]->guid(), kGuid1);
EXPECT_EQ(bookmark_bar_node->children()[1]->GetTitle(),
base::UTF8ToUTF16(kNewTitle));
EXPECT_EQ(bookmark_bar_node->children()[1]->children().size(), 1u);
EXPECT_THAT(tracker->TrackedEntitiesCountForTest(), Eq(4U));
}
TEST(BookmarkModelMergerTest,
ShouldIgnoreFolderSemanticsMatchAndLaterMatchByGUIDWithGUIDNodeFirst) {
const std::string kFolderId1 = "Folder Id 1";
const std::string kFolderId2 = "Folder Id 2";
const std::string kOriginalTitle = "Original Title";
const std::string kNewTitle = "New Title";
const base::GUID kGuid1 = base::GUID::GenerateRandomV4();
const base::GUID kGuid2 = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- The local model --------
// bookmark_bar
// | - folder (kGuid1/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,
base::UTF8ToUTF16(kOriginalTitle), /*meta_info=*/nullptr,
/*creation_time=*/base::Time::Now(), kGuid1);
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 (kGuid1/kNewTitle)
// | - folder (kGuid2/kOriginalTitle)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
const std::string 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 GUID and not
// semantics.
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid1, /*parent_guid=*/BookmarkBarGuid(), kNewTitle,
/*url=*/"",
/*is_folder=*/true,
/*unique_position=*/pos1));
// Add a remote folder to correspond to the local folder by
// semantics and not GUID.
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid2, /*parent_guid=*/BookmarkBarGuid(), kOriginalTitle,
/*url=*/"",
/*is_folder=*/true,
/*unique_position=*/pos2));
Merge(std::move(updates), bookmark_model.get());
// -------- The merged model --------
// bookmark_bar
// | - folder (kGuid1/kNewTitle)
// | - folder (kGuid2/kOriginalTitle)
// Node should have been merged with its GUID match.
ASSERT_EQ(bookmark_bar_node->children().size(), 2u);
EXPECT_EQ(bookmark_bar_node->children()[0]->guid(), kGuid1);
EXPECT_EQ(bookmark_bar_node->children()[0]->GetTitle(),
base::UTF8ToUTF16(kNewTitle));
EXPECT_EQ(bookmark_bar_node->children()[0]->children().size(), 1u);
EXPECT_EQ(bookmark_bar_node->children()[1]->guid(), kGuid2);
EXPECT_EQ(bookmark_bar_node->children()[1]->GetTitle(),
base::UTF8ToUTF16(kOriginalTitle));
EXPECT_EQ(bookmark_bar_node->children()[1]->children().size(), 0u);
}
TEST(BookmarkModelMergerTest, ShouldReplaceBookmarkGUIDWithConflictingURLs) {
const std::string kTitle = "Title";
const std::string kUrl1 = "http://www.foo.com/";
const std::string kUrl2 = "http://www.bar.com/";
const base::GUID kGuid = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- The local model --------
// bookmark_bar
// | - bookmark (kGuid/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, base::UTF8ToUTF16(kTitle),
GURL(kUrl1), /*meta_info=*/nullptr, base::Time::Now(), kGuid);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// | - bookmark (kGuid/kUrl2)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData( // Remote B
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), kTitle,
/*url=*/kUrl2,
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
Merge(std::move(updates), bookmark_model.get());
// -------- The merged model --------
// bookmark_bar
// | - bookmark (kGuid/kUrl2)
// | - bookmark ([new GUID]/kUrl1)
// Conflicting node GUID should have been replaced.
ASSERT_EQ(bookmark_bar_node->children().size(), 2u);
EXPECT_EQ(bookmark_bar_node->children()[0]->guid(), kGuid);
EXPECT_EQ(bookmark_bar_node->children()[0]->url(), kUrl2);
EXPECT_NE(bookmark_bar_node->children()[1]->guid(), kGuid);
EXPECT_EQ(bookmark_bar_node->children()[1]->url(), kUrl1);
}
TEST(BookmarkModelMergerTest, ShouldReplaceBookmarkGUIDWithConflictingTypes) {
const std::string kTitle = "Title";
const base::GUID kGuid = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- The local model --------
// bookmark_bar
// | - bookmark (kGuid)
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model->bookmark_bar_node();
const bookmarks::BookmarkNode* bookmark = bookmark_model->AddURL(
/*parent=*/bookmark_bar_node, /*index=*/0, base::UTF8ToUTF16(kTitle),
GURL("http://www.foo.com/"), /*meta_info=*/nullptr, base::Time::Now(),
kGuid);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// | - folder(kGuid)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData( // Remote B
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), kTitle,
/*url=*/"",
/*is_folder=*/true,
/*unique_position=*/MakeRandomPosition()));
Merge(std::move(updates), bookmark_model.get());
// -------- The merged model --------
// bookmark_bar
// | - folder (kGuid)
// | - bookmark ([new GUID])
// Conflicting node GUID should have been replaced.
ASSERT_EQ(bookmark_bar_node->children().size(), 2u);
EXPECT_EQ(bookmark_bar_node->children()[0]->guid(), kGuid);
EXPECT_TRUE(bookmark_bar_node->children()[0]->is_folder());
EXPECT_NE(bookmark_bar_node->children()[1]->guid(), kGuid);
EXPECT_FALSE(bookmark_bar_node->children()[1]->is_folder());
}
TEST(BookmarkModelMergerTest,
ShouldReplaceBookmarkGUIDWithConflictingTypesAndLocalChildren) {
const base::GUID kGuid1 = base::GUID::GenerateRandomV4();
const base::GUID kGuid2 = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- The local model --------
// bookmark_bar
// | - folder (kGuid1)
// | - bookmark (kGuid2)
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(), kGuid1);
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(), kGuid2);
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 (kGuid1)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid1, /*parent_guid=*/BookmarkBarGuid(), "Bar's title",
"http://bar.com/",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
Merge(std::move(updates), bookmark_model.get());
// -------- The merged model --------
// bookmark_bar
// | - bookmark (kGuid1)
// | - folder ([new GUID])
// | - bookmark (kGuid2)
// Conflicting node GUID should have been replaced.
ASSERT_EQ(bookmark_bar_node->children().size(), 2u);
EXPECT_EQ(bookmark_bar_node->children()[0]->guid(), kGuid1);
EXPECT_NE(bookmark_bar_node->children()[1]->guid(), kGuid1);
EXPECT_NE(bookmark_bar_node->children()[1]->guid(), kGuid2);
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]->guid(), kGuid2);
}
// Tests that the GUID-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, ShouldIgnoreRemoteGUIDIfOrphanNode) {
const std::string kInexistentParentId = "InexistentParentId";
const std::string kTitle = "Title";
const std::string kUrl = "http://www.foo.com/";
const base::GUID kGuid = base::GUID::GenerateRandomV4();
const base::GUID kInexistentParentGuid = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- The local model --------
// bookmark_bar
// | - bookmark(kGuid/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, base::UTF8ToUTF16(kTitle),
GURL(kUrl), /*meta_info=*/nullptr, base::Time::Now(), kGuid);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// Orphan node: bookmark(kGuid/kTitle)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid, /*parent_guid=*/kInexistentParentGuid, kTitle,
/*url=*/kUrl,
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), bookmark_model.get());
// -------- The merged model --------
// bookmark_bar
// |- bookmark(kGuid/kTitle)
// The local node should have been tracked.
EXPECT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
EXPECT_EQ(bookmark->GetTitle(), base::UTF8ToUTF16(kTitle));
EXPECT_THAT(tracker->GetEntityForBookmarkNode(bookmark), NotNull());
EXPECT_THAT(tracker->GetEntityForGUID(kGuid), NotNull());
EXPECT_THAT(tracker->GetEntityForGUID(kInexistentParentGuid), IsNull());
}
// Tests that the GUID-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, ShouldIgnoreRemoteGUIDIfInvalidSpecifics) {
const std::string kTitle = "Title";
const std::string kLocalUrl = "http://www.foo.com/";
const std::string kInvalidUrl = "invalidurl";
const base::GUID kGuid = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- The local model --------
// bookmark_bar
// | - bookmark(kGuid/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, base::UTF8ToUTF16(kTitle),
GURL(kLocalUrl), /*meta_info=*/nullptr, base::Time::Now(), kGuid);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// | - bookmark (kGuid/kInvalidURL/kTitle)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), kTitle,
/*url=*/kInvalidUrl,
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), bookmark_model.get());
// -------- The merged model --------
// bookmark_bar
// |- bookmark(kGuid/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(), base::UTF8ToUTF16(kTitle));
EXPECT_THAT(tracker->GetEntityForBookmarkNode(bookmark), NotNull());
}
// Tests that updates with a GUID that is different to originator client item ID
// are ignored.
TEST(BookmarkModelMergerTest, ShouldIgnoreRemoteUpdateWithInvalidGUID) {
const base::GUID kGuid1 = base::GUID::GenerateRandomV4();
const base::GUID kGuid2 = base::GUID::GenerateRandomV4();
const std::string kTitle1 = "Title1";
const std::string kTitle2 = "Title2";
const std::string kLocalTitle = "LocalTitle";
const std::string kUrl = "http://www.foo.com/";
const base::GUID kGuid = base::GUID::GenerateRandomV4();
const base::GUID kUnexpectedOriginatorItemId = base::GUID::GenerateRandomV4();
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- The local model --------
// | - bookmark(kGuid/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, base::UTF8ToUTF16(kLocalTitle),
GURL(kUrl), /*meta_info=*/nullptr, base::Time::Now(), kGuid);
ASSERT_TRUE(bookmark);
ASSERT_THAT(bookmark_bar_node->children(), ElementRawPointersAre(bookmark));
// -------- The remote model --------
// bookmark_bar
// | - bookmark (kGuid/kUrl/kTitle1)
// | - bookmark (kGuid/kUrl/kTitle2)
const std::string 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(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), kTitle1,
/*url=*/kUrl,
/*is_folder=*/false, /*unique_position=*/position1));
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), kTitle2,
/*url=*/kUrl,
/*is_folder=*/false, /*unique_position=*/position2));
// |originator_client_item_id| cannot itself be duplicated because
// ModelTypeWorker guarantees otherwise.
updates.back().entity.originator_client_item_id =
kUnexpectedOriginatorItemId.AsLowercaseString();
updates.back().entity.id =
GetFakeServerIdFromGUID(kUnexpectedOriginatorItemId);
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), bookmark_model.get());
// -------- The merged model --------
// | - bookmark (kGuid/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->guid(), Eq(kGuid));
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::string kFolder1Title = "folder1";
const std::string kFolder2Title = "folder2";
const std::string kUrl1Title = "url1";
const std::string kUrl2Title = "url2";
const std::string kUrl1 = "http://www.url1.com/";
const std::string kUrl2 = "http://www.url2.com/";
const base::GUID kFolder1Guid = base::GUID::GenerateRandomV4();
const base::GUID kFolder2Guid = base::GUID::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)
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model->bookmark_bar_node();
const bookmarks::BookmarkNode* folder1 = bookmark_model->AddFolder(
/*parent=*/bookmark_bar_node, /*index=*/0,
base::UTF8ToUTF16(kFolder1Title));
const bookmarks::BookmarkNode* folder1_url1_node = bookmark_model->AddURL(
/*parent=*/folder1, /*index=*/0, base::UTF8ToUTF16(kUrl1Title),
GURL(kUrl1));
bookmark_model->AddURL(
/*parent=*/folder1, /*index=*/1, base::UTF8ToUTF16(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 GUID. 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 std::string 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(
/*guid=*/kFolder1Guid, /*parent_guid=*/BookmarkBarGuid(), kFolder1Title,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder1));
updates.push_back(CreateUpdateResponseData(
/*guid=*/kFolder2Guid, /*parent_guid=*/BookmarkBarGuid(), kFolder2Title,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder2));
updates.push_back(CreateUpdateResponseData(
/*guid=*/folder1_url1_node->guid(), /*parent_guid=*/kFolder2Guid,
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.get());
ASSERT_THAT(bookmark_bar_node->children().size(), Eq(2u));
// Verify Folder 1.
EXPECT_THAT(bookmark_bar_node->children()[0]->GetTitle(),
Eq(base::ASCIIToUTF16(kFolder1Title)));
ASSERT_THAT(bookmark_bar_node->children()[0]->children().size(), Eq(1u));
EXPECT_THAT(bookmark_bar_node->children()[0]->children()[0]->GetTitle(),
Eq(base::ASCIIToUTF16(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(base::ASCIIToUTF16(kFolder2Title)));
ASSERT_THAT(bookmark_bar_node->children()[1]->children().size(), Eq(1u));
EXPECT_THAT(bookmark_bar_node->children()[1]->children()[0]->GetTitle(),
Eq(base::ASCIIToUTF16(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) {
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- The remote model --------
// bookmark_bar
// | - bookmark (<invalid url>)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/base::GUID::GenerateRandomV4(),
/*parent_guid=*/BookmarkBarGuid(), "Title",
/*url=*/"invalidurl",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
base::HistogramTester histogram_tester;
Merge(std::move(updates), bookmark_model.get());
histogram_tester.ExpectUniqueSample(
"Sync.ProblematicServerSideBookmarksDuringMerge",
/*sample=*/ExpectedRemoteBookmarkUpdateError::kInvalidSpecifics,
/*count=*/1);
}
TEST(BookmarkModelMergerTest, ShouldLogMetricsForChildrenOfNonFolder) {
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
const base::GUID kGuid = base::GUID::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(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), "Title1",
/*url=*/"http://url1",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
updates.push_back(CreateUpdateResponseData(
/*guid=*/base::GUID::GenerateRandomV4(), /*parent_guid=*/kGuid, "Title2",
/*url=*/"http://url2",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
updates.push_back(CreateUpdateResponseData(
/*guid=*/base::GUID::GenerateRandomV4(), /*parent_guid=*/kGuid, "Title3",
/*url=*/"http://url3",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
updates.push_back(CreateUpdateResponseData(
/*guid=*/base::GUID::GenerateRandomV4(), /*parent_guid=*/kGuid, "Title4",
/*url=*/"http://url4",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
base::HistogramTester histogram_tester;
Merge(std::move(updates), bookmark_model.get());
histogram_tester.ExpectUniqueSample(
"Sync.ProblematicServerSideBookmarksDuringMerge",
/*sample=*/ExpectedRemoteBookmarkUpdateError::kParentNotFolder,
/*count=*/3);
}
TEST(BookmarkModelMergerTest, ShouldLogMetricsForChildrenOfOrphanUpdates) {
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- The remote model --------
// bookmark_bar
// Orphan node: bookmark(url1/title1)
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/base::GUID::GenerateRandomV4(),
/*parent_guid=*/base::GUID::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.get());
ASSERT_THAT(tracker, NotNull());
EXPECT_THAT(histogram_tester.GetTotalSum(
"Sync.BookmarkModelMerger.ValidInputUpdates"),
Eq(2));
histogram_tester.ExpectUniqueSample(
"Sync.ProblematicServerSideBookmarksDuringMerge",
/*sample=*/ExpectedRemoteBookmarkUpdateError::kMissingParentEntity,
/*count=*/1);
EXPECT_THAT(histogram_tester.GetTotalSum(
"Sync.BookmarkModelMerger.ReachableInputUpdates"),
Eq(1));
EXPECT_THAT(tracker->GetNumIgnoredUpdatesDueToMissingParentForTest(), Eq(1));
}
TEST(BookmarkModelMergerTest, ShouldLogMetricsForUnsupportedServerTag) {
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
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.get());
histogram_tester.ExpectUniqueSample(
"Sync.ProblematicServerSideBookmarksDuringMerge",
/*sample=*/ExpectedRemoteBookmarkUpdateError::kUnsupportedPermanentFolder,
/*count=*/1);
}
TEST(BookmarkModelMergerTest, ShouldLogMetricsForkDescendantOfRootNode) {
const std::string kRootNodeId = "test_root_node_id";
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- 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::ModelTypeToRootTag(syncer::BOOKMARKS);
updates.push_back(CreateUpdateResponseData(
/*guid=*/base::GUID::GenerateRandomV4(),
base::GUID::ParseLowercase(bookmarks::BookmarkNode::kRootNodeGuid),
"Title1",
/*url=*/"http://url1",
/*is_folder=*/false,
/*unique_position=*/MakeRandomPosition()));
base::HistogramTester histogram_tester;
Merge(std::move(updates), bookmark_model.get());
histogram_tester.ExpectUniqueSample(
"Sync.ProblematicServerSideBookmarksDuringMerge",
/*sample=*/ExpectedRemoteBookmarkUpdateError::kMissingParentEntity,
/*count=*/1);
}
TEST(BookmarkModelMergerTest, ShouldRemoveMatchingDuplicatesByGUID) {
const std::string kTitle1 = "Title 1";
const std::string kTitle2 = "Title 2";
const std::string kTitle3 = "Title 3";
const std::string kUrl = "http://www.url.com/";
const base::GUID kUrlGUID = base::GUID::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, UrlGUID)
// |- url2(http://www.url.com, UrlGUID)
// |- url3(http://www.url.com, <other-guid>)
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kUrlGUID, /*parent_guid=*/BookmarkBarGuid(), 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(
/*guid=*/kUrlGUID, /*parent_guid=*/BookmarkBarGuid(), 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(
/*guid=*/base::GUID::GenerateRandomV4(),
/*parent_guid=*/BookmarkBarGuid(), 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.get());
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model->bookmark_bar_node();
EXPECT_THAT(bookmark_bar_node->children(),
UnorderedElementsAre(HasTitle(base::UTF8ToUTF16(kTitle2)),
HasTitle(base::UTF8ToUTF16(kTitle3))));
EXPECT_THAT(histogram_tester.GetTotalSum(
"Sync.BookmarkModelMerger.ValidInputUpdates"),
Eq(4));
histogram_tester.ExpectBucketCount(
"Sync.BookmarksGUIDDuplicates",
/*sample=*/ExpectedBookmarksGUIDDuplicates::kMatchingUrls, /*count=*/1);
EXPECT_THAT(histogram_tester.GetTotalSum(
"Sync.BookmarkModelMerger.ReachableInputUpdates"),
Eq(3));
}
TEST(BookmarkModelMergerTest, ShouldRemoveDifferentDuplicatesByGUID) {
const std::string kTitle1 = "Title 1";
const std::string kTitle2 = "Title 2";
const std::string kUrl = "http://www.url.com/";
const std::string kDifferentUrl = "http://www.different-url.com/";
const base::GUID kUrlGUID = base::GUID::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, UrlGUID)
// |- url2(http://www.different-url.com, UrlGUID)
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kUrlGUID, /*parent_guid=*/BookmarkBarGuid(), 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(
/*guid=*/kUrlGUID, /*parent_guid=*/BookmarkBarGuid(), 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.get());
const bookmarks::BookmarkNode* bookmark_bar_node =
bookmark_model->bookmark_bar_node();
EXPECT_THAT(bookmark_bar_node->children(),
UnorderedElementsAre(HasTitle(base::UTF8ToUTF16(kTitle1))));
histogram_tester.ExpectBucketCount(
"Sync.BookmarksGUIDDuplicates",
/*sample=*/ExpectedBookmarksGUIDDuplicates::kDifferentUrls, /*count=*/1);
}
TEST(BookmarkModelMergerTest, ShouldRemoveMatchingFolderDuplicatesByGUID) {
const std::string kTitle = "Title";
const base::GUID kGuid = base::GUID::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, GUID)
// |- folder2(Title, GUID)
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), 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(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), 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.get());
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=*/ExpectedBookmarksGUIDDuplicates::kMatchingFolders,
/*count=*/1);
EXPECT_THAT(tracker->GetEntityForSyncId("Id1"), IsNull());
EXPECT_THAT(tracker->GetEntityForSyncId("Id2"), NotNull());
}
TEST(BookmarkModelMergerTest, ShouldRemoveDifferentFolderDuplicatesByGUID) {
const std::string kTitle1 = "Title 1";
const std::string kTitle2 = "Title 2";
const base::GUID kGuid = base::GUID::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, GUID)
// |- folder11
// |- folder2(Title, GUID)
// |- folder21
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), kTitle1,
/*url=*/"",
/*is_folder=*/true, MakeRandomPosition()));
updates.back().entity.id = "Id1";
updates.back().entity.creation_time = base::Time::Now();
updates.push_back(CreateUpdateResponseData(
/*guid=*/base::GUID::GenerateRandomV4(), /*parent_guid=*/kGuid,
"Some title",
/*url=*/"", /*is_folder=*/true, MakeRandomPosition()));
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), 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(
/*guid=*/base::GUID::GenerateRandomV4(), /*parent_guid=*/kGuid,
"Some title 2",
/*url=*/"", /*is_folder=*/true, MakeRandomPosition()));
base::HistogramTester histogram_tester;
std::unique_ptr<SyncedBookmarkTracker> tracker =
Merge(std::move(updates), bookmark_model.get());
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=*/ExpectedBookmarksGUIDDuplicates::kDifferentFolders,
/*count=*/1);
EXPECT_THAT(tracker->GetEntityForSyncId("Id1"), NotNull());
EXPECT_THAT(tracker->GetEntityForSyncId("Id2"), IsNull());
EXPECT_EQ(bookmark_bar_node->children().front()->GetTitle(),
base::UTF8ToUTF16(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::string kLocalTitle = "local";
const std::string kRemoteTitle = "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;
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- 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::UTF8ToUTF16(kLocalTitle));
ASSERT_TRUE(folder);
// -------- The remote model --------
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
base::GUID parent_guid = BookmarkBarGuid();
// Create a tree with depth |kRemoteUpdatesDepth| to verify the limit of
// kMaxBookmarkTreeDepth is enforced.
for (size_t i = 1; i < kRemoteUpdatesDepth; ++i) {
base::GUID folder_guid = base::GUID::GenerateRandomV4();
updates.push_back(CreateUpdateResponseData(
/*guid=*/folder_guid, /*parent_guid=*/parent_guid, kRemoteTitle,
/*url=*/"",
/*is_folder=*/true, MakeRandomPosition()));
parent_guid = folder_guid;
}
ASSERT_THAT(updates.size(), Eq(kRemoteUpdatesDepth));
std::unique_ptr<SyncedBookmarkTracker> tracker =
SyncedBookmarkTracker::CreateEmpty(sync_pb::ModelTypeState());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
BookmarkModelMerger(std::move(updates), bookmark_model.get(),
&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;
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();
const std::string suffix = syncer::UniquePosition::RandomSuffix();
const syncer::UniquePosition posFolder1 =
syncer::UniquePosition::InitialPosition(suffix);
const syncer::UniquePosition posFolder2 =
syncer::UniquePosition::After(posFolder1, suffix);
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
// -------- The remote model --------
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kFolder1Guid, /*parent_guid=*/BookmarkBarGuid(), 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(
/*guid=*/kFolder2Guid, /*parent_guid=*/BookmarkBarGuid(), 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.get());
ASSERT_THAT(tracker->GetEntityForGUID(kFolder1Guid), NotNull());
ASSERT_THAT(tracker->GetEntityForGUID(kFolder2Guid), NotNull());
EXPECT_TRUE(tracker->GetEntityForGUID(kFolder1Guid)->IsUnsynced());
EXPECT_FALSE(tracker->GetEntityForGUID(kFolder2Guid)->IsUnsynced());
EXPECT_THAT(histogram_tester.GetTotalSum(
"Sync.BookmarkModelMerger.UnsyncedEntitiesUponCompletion"),
Eq(1));
}
TEST(BookmarkModelMergerTest, ShouldRemoveDifferentTypeDuplicatesByGUID) {
const std::string kTitle = "Title";
const base::GUID kGuid = base::GUID::GenerateRandomV4();
// The remote model has 2 duplicates, a folder and a URL.
//
// -------- The remote model --------
// bookmark_bar
// |- folder1(GUID)
// |- folder11
// |- URL1(GUID)
std::unique_ptr<bookmarks::BookmarkModel> bookmark_model =
bookmarks::TestBookmarkClient::CreateModel();
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkBarNodeUpdateData());
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), kTitle,
/*url=*/"",
/*is_folder=*/true, MakeRandomPosition()));
updates.back().entity.id = "Id1";
updates.push_back(CreateUpdateResponseData(
/*guid=*/base::GUID::GenerateRandomV4(), /*parent_guid=*/kGuid,
"Some title",
/*url=*/"", /*is_folder=*/true, MakeRandomPosition()));
updates.push_back(CreateUpdateResponseData(
/*guid=*/kGuid, /*parent_guid=*/BookmarkBarGuid(), 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.get());
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=*/ExpectedBookmarksGUIDDuplicates::kDifferentTypes,
/*count=*/1);
EXPECT_THAT(tracker->GetEntityForSyncId("Id1"), NotNull());
EXPECT_THAT(tracker->GetEntityForSyncId("Id2"), IsNull());
EXPECT_EQ(bookmark_bar_node->children().front()->children().size(), 1u);
}
} // namespace sync_bookmarks