blob: 54cdd4429e303b68948c10715e519f6448376790 [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/strings/utf_string_conversions.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/test/test_bookmark_client.h"
#include "components/favicon/core/test/mock_favicon_service.h"
#include "components/sync/base/unique_position.h"
#include "components/sync_bookmarks/synced_bookmark_tracker.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using testing::_;
using testing::Eq;
using testing::UnorderedElementsAre;
namespace sync_bookmarks {
namespace {
const char kBookmarkBarId[] = "bookmark_bar_id";
const char kBookmarkBarTag[] = "bookmark_bar";
std::unique_ptr<syncer::UpdateResponseData> CreateUpdateResponseData(
const std::string& server_id,
const std::string& parent_id,
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()) {
auto data = std::make_unique<syncer::EntityData>();
data->id = server_id;
data->parent_id = parent_id;
data->unique_position = unique_position.ToProto();
sync_pb::BookmarkSpecifics* bookmark_specifics =
data->specifics.mutable_bookmark();
bookmark_specifics->set_title(title);
bookmark_specifics->set_url(url);
bookmark_specifics->set_icon_url(icon_url);
bookmark_specifics->set_favicon(icon_data);
data->is_folder = is_folder;
auto response_data = std::make_unique<syncer::UpdateResponseData>();
response_data->entity = std::move(data);
// Similar to what's done in the loopback_server.
response_data->response_version = 0;
return response_data;
}
std::unique_ptr<syncer::UpdateResponseData> CreateBookmarkBarNodeUpdateData() {
auto data = std::make_unique<syncer::EntityData>();
data->id = kBookmarkBarId;
data->server_defined_unique_tag = kBookmarkBarTag;
data->specifics.mutable_bookmark();
auto response_data = std::make_unique<syncer::UpdateResponseData>();
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 SyncedBookmarkTracker::Entity* 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->GetChild(0), 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);
});
}
} // namespace
TEST(BookmarkModelMergerTest, ShouldMergeLocalAndRemoteModels) {
const size_t kMaxEntries = 1000;
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 std::string kFolder1Id = "Folder1Id";
const std::string kFolder3Id = "Folder3Id";
const std::string kUrl1Id = "Url1Id";
const std::string kUrl2Id = "Url2Id";
const std::string kUrl3Id = "Url3Id";
const std::string kUrl4Id = "Url4Id";
// -------- 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(
/*server_id=*/kFolder1Id, /*parent_id=*/kBookmarkBarId, kFolder1Title,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder1));
updates.push_back(CreateUpdateResponseData(
/*server_id=*/kUrl1Id, /*parent_id=*/kFolder1Id, kUrl1Title, kUrl1,
/*is_folder=*/false, /*unique_position=*/posUrl1));
updates.push_back(CreateUpdateResponseData(
/*server_id=*/kUrl2Id, /*parent_id=*/kFolder1Id, kUrl2Title, kAnotherUrl2,
/*is_folder=*/false, /*unique_position=*/posUrl2));
updates.push_back(CreateUpdateResponseData(
/*server_id=*/kFolder3Id, /*parent_id=*/kBookmarkBarId, kFolder3Title,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder3));
updates.push_back(CreateUpdateResponseData(
/*server_id=*/kUrl3Id, /*parent_id=*/kFolder3Id, kUrl3Title, kUrl3,
/*is_folder=*/false, /*unique_position=*/posUrl3));
updates.push_back(CreateUpdateResponseData(
/*server_id=*/kUrl4Id, /*parent_id=*/kFolder3Id, 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)
SyncedBookmarkTracker tracker(std::vector<NodeMetadataPair>(),
std::make_unique<sync_pb::ModelTypeState>());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
BookmarkModelMerger(&updates, bookmark_model.get(), &favicon_service,
&tracker)
.Merge();
ASSERT_THAT(bookmark_bar_node->children().size(), Eq(3u));
// Verify Folder 1.
EXPECT_THAT(bookmark_bar_node->GetChild(0)->GetTitle(),
Eq(base::ASCIIToUTF16(kFolder1Title)));
ASSERT_THAT(bookmark_bar_node->GetChild(0)->children().size(), Eq(3u));
EXPECT_THAT(bookmark_bar_node->GetChild(0)->GetChild(0)->GetTitle(),
Eq(base::ASCIIToUTF16(kUrl1Title)));
EXPECT_THAT(bookmark_bar_node->GetChild(0)->GetChild(0)->url(),
Eq(GURL(kUrl1)));
EXPECT_THAT(bookmark_bar_node->GetChild(0)->GetChild(1)->GetTitle(),
Eq(base::ASCIIToUTF16(kUrl2Title)));
EXPECT_THAT(bookmark_bar_node->GetChild(0)->GetChild(1)->url(),
Eq(GURL(kAnotherUrl2)));
EXPECT_THAT(bookmark_bar_node->GetChild(0)->GetChild(2)->GetTitle(),
Eq(base::ASCIIToUTF16(kUrl2Title)));
EXPECT_THAT(bookmark_bar_node->GetChild(0)->GetChild(2)->url(),
Eq(GURL(kUrl2)));
// Verify Folder 3.
EXPECT_THAT(bookmark_bar_node->GetChild(1)->GetTitle(),
Eq(base::ASCIIToUTF16(kFolder3Title)));
ASSERT_THAT(bookmark_bar_node->GetChild(1)->children().size(), Eq(2u));
EXPECT_THAT(bookmark_bar_node->GetChild(1)->GetChild(0)->GetTitle(),
Eq(base::ASCIIToUTF16(kUrl3Title)));
EXPECT_THAT(bookmark_bar_node->GetChild(1)->GetChild(0)->url(),
Eq(GURL(kUrl3)));
EXPECT_THAT(bookmark_bar_node->GetChild(1)->GetChild(1)->GetTitle(),
Eq(base::ASCIIToUTF16(kUrl4Title)));
EXPECT_THAT(bookmark_bar_node->GetChild(1)->GetChild(1)->url(),
Eq(GURL(kUrl4)));
// Verify Folder 2.
EXPECT_THAT(bookmark_bar_node->GetChild(2)->GetTitle(),
Eq(base::ASCIIToUTF16(kFolder2Title)));
ASSERT_THAT(bookmark_bar_node->GetChild(2)->children().size(), Eq(2u));
EXPECT_THAT(bookmark_bar_node->GetChild(2)->GetChild(0)->GetTitle(),
Eq(base::ASCIIToUTF16(kUrl3Title)));
EXPECT_THAT(bookmark_bar_node->GetChild(2)->GetChild(0)->url(),
Eq(GURL(kUrl3)));
EXPECT_THAT(bookmark_bar_node->GetChild(2)->GetChild(1)->GetTitle(),
Eq(base::ASCIIToUTF16(kUrl4Title)));
EXPECT_THAT(bookmark_bar_node->GetChild(2)->GetChild(1)->url(),
Eq(GURL(kUrl4)));
// Verify the tracker contents.
EXPECT_THAT(tracker.TrackedEntitiesCountForTest(), Eq(11U));
std::vector<const SyncedBookmarkTracker::Entity*> local_changes =
tracker.GetEntitiesWithLocalChanges(kMaxEntries);
EXPECT_THAT(local_changes.size(), Eq(4U));
std::vector<const bookmarks::BookmarkNode*> nodes_with_local_changes;
for (const SyncedBookmarkTracker::Entity* 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->GetChild(0)->GetChild(2),
bookmark_bar_node->GetChild(2),
bookmark_bar_node->GetChild(2)->GetChild(0),
bookmark_bar_node->GetChild(2)->GetChild(1)));
// Verify positions in tracker.
EXPECT_TRUE(PositionsInTrackerMatchModel(bookmark_bar_node, tracker));
}
TEST(BookmarkModelMergerTest, ShouldMergeRemoteReorderToLocalModel) {
const size_t kMaxEntries = 1000;
const std::string kFolder1Title = "folder1";
const std::string kFolder2Title = "folder2";
const std::string kFolder3Title = "folder3";
const std::string kFolder1Id = "Folder1Id";
const std::string kFolder2Id = "Folder2Id";
const std::string kFolder3Id = "Folder3Id";
// -------- 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(
/*server_id=*/kFolder1Id, /*parent_id=*/kBookmarkBarId, kFolder1Title,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder1));
updates.push_back(CreateUpdateResponseData(
/*server_id=*/kFolder2Id, /*parent_id=*/kBookmarkBarId, kFolder2Title,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder2));
updates.push_back(CreateUpdateResponseData(
/*server_id=*/kFolder3Id, /*parent_id=*/kBookmarkBarId, kFolder3Title,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/posFolder3));
// -------- The expected merge outcome --------
// bookmark_bar
// |- folder 1
// |- folder 3
// |- folder 2
SyncedBookmarkTracker tracker(std::vector<NodeMetadataPair>(),
std::make_unique<sync_pb::ModelTypeState>());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
BookmarkModelMerger(&updates, bookmark_model.get(), &favicon_service,
&tracker)
.Merge();
ASSERT_THAT(bookmark_bar_node->children().size(), Eq(3u));
EXPECT_THAT(bookmark_bar_node->GetChild(0)->GetTitle(),
Eq(base::ASCIIToUTF16(kFolder1Title)));
EXPECT_THAT(bookmark_bar_node->GetChild(1)->GetTitle(),
Eq(base::ASCIIToUTF16(kFolder3Title)));
EXPECT_THAT(bookmark_bar_node->GetChild(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 SyncedBookmarkTracker::Entity*> local_changes =
tracker.GetEntitiesWithLocalChanges(kMaxEntries);
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 std::string kId2 = "Id2";
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(
/*server_id=*/kId2, /*parent_id=*/kBookmarkBarId, kTitle2, kUrl2.spec(),
/*is_folder=*/false, /*unique_position=*/pos2, kIcon2Url.spec(),
/*icon_data=*/"PNG"));
// -------- The expected merge outcome --------
// bookmark_bar
// |- title 2
// |- title 1
SyncedBookmarkTracker tracker(std::vector<NodeMetadataPair>(),
std::make_unique<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(&updates, bookmark_model.get(), &favicon_service,
&tracker)
.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 std::string kId = "Id";
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(CreateUpdateResponseData(
/*server_id=*/kId, /*parent_id=*/kBookmarkBarId, kRemoteTitle,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/pos));
SyncedBookmarkTracker tracker(std::vector<NodeMetadataPair>(),
std::make_unique<sync_pb::ModelTypeState>());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
BookmarkModelMerger(&updates, bookmark_model.get(), &favicon_service,
&tracker)
.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 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 std::string kId = "Id";
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(
/*server_id=*/kId, /*parent_id=*/kBookmarkBarId, kRemoteTruncatedTitle,
/*url=*/std::string(),
/*is_folder=*/true, /*unique_position=*/pos));
SyncedBookmarkTracker tracker(std::vector<NodeMetadataPair>(),
std::make_unique<sync_pb::ModelTypeState>());
testing::NiceMock<favicon::MockFaviconService> favicon_service;
BookmarkModelMerger(&updates, bookmark_model.get(), &favicon_service,
&tracker)
.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));
}
} // namespace sync_bookmarks