blob: 7a3ed9d2d83f76258828123317311325f72c7544 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/sync_bookmarks/local_bookmark_model_merger.h"
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "base/test/scoped_feature_list.h"
#include "base/uuid.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/browser/bookmark_test_util.h"
#include "components/bookmarks/test/test_bookmark_client.h"
#include "components/bookmarks/test/test_matchers.h"
#include "components/sync/base/features.h"
#include "components/sync_bookmarks/bookmark_model_view.h"
#include "components/sync_bookmarks/test_bookmark_model_view.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "url/gurl.h"
namespace sync_bookmarks {
namespace {
using bookmarks::test::IsFolder;
using bookmarks::test::IsFolderWithUuid;
using bookmarks::test::IsUrlBookmark;
using bookmarks::test::IsUrlBookmarkWithUuid;
using testing::ElementsAre;
using testing::IsEmpty;
using testing::Ne;
// Test class to build bookmark URLs conveniently and compactly in tests.
class UrlBuilder {
public:
UrlBuilder(const std::u16string& title, const GURL& url)
: title_(title), url_(url) {}
UrlBuilder(const UrlBuilder&) = default;
~UrlBuilder() = default;
UrlBuilder& SetUuid(const base::Uuid& uuid) {
uuid_ = uuid;
return *this;
}
void Build(BookmarkModelView* model,
const bookmarks::BookmarkNode* parent) const {
model->AddURL(parent, parent->children().size(), title_, url_,
/*meta_info=*/nullptr, /*creation_time=*/std::nullopt, uuid_);
}
private:
const std::u16string title_;
const GURL url_;
std::optional<base::Uuid> uuid_;
};
// Test class to build bookmark folders and compactly in tests.
class FolderBuilder {
public:
using FolderOrUrl = absl::variant<FolderBuilder, UrlBuilder>;
static void AddChildTo(BookmarkModelView* model,
const bookmarks::BookmarkNode* parent,
const FolderOrUrl& folder_or_url) {
if (absl::holds_alternative<UrlBuilder>(folder_or_url)) {
absl::get<UrlBuilder>(folder_or_url).Build(model, parent);
} else {
CHECK(absl::holds_alternative<FolderBuilder>(folder_or_url));
absl::get<FolderBuilder>(folder_or_url).Build(model, parent);
}
}
static void AddChildrenTo(BookmarkModelView* model,
const bookmarks::BookmarkNode* parent,
const std::vector<FolderOrUrl>& children) {
for (const FolderOrUrl& folder_or_url : children) {
AddChildTo(model, parent, folder_or_url);
}
}
explicit FolderBuilder(const std::u16string& title) : title_(title) {}
FolderBuilder(const FolderBuilder&) = default;
~FolderBuilder() = default;
FolderBuilder& SetChildren(std::vector<FolderOrUrl> children) {
children_ = std::move(children);
return *this;
}
FolderBuilder& SetUuid(const base::Uuid& uuid) {
uuid_ = uuid;
return *this;
}
void Build(BookmarkModelView* model,
const bookmarks::BookmarkNode* parent) const {
const bookmarks::BookmarkNode* folder = model->AddFolder(
parent, parent->children().size(), title_,
/*meta_info=*/nullptr, /*creation_time=*/std::nullopt, uuid_);
AddChildrenTo(model, folder, children_);
}
private:
const std::u16string title_;
std::vector<FolderOrUrl> children_;
std::optional<base::Uuid> uuid_;
};
std::unique_ptr<TestBookmarkModelView> BuildLocalModel(
const std::vector<FolderBuilder::FolderOrUrl>& children_of_bookmark_bar) {
auto model = std::make_unique<TestBookmarkModelView>(
TestBookmarkModelView::ViewType::kLocalOrSyncableNodes);
FolderBuilder::AddChildrenTo(model.get(), model->bookmark_bar_node(),
children_of_bookmark_bar);
return model;
}
std::unique_ptr<TestBookmarkModelView> BuildAccountModel(
const std::vector<FolderBuilder::FolderOrUrl>& children_of_bookmark_bar) {
auto model = std::make_unique<TestBookmarkModelView>(
TestBookmarkModelView::ViewType::kAccountNodes);
model->EnsurePermanentNodesExist();
FolderBuilder::AddChildrenTo(model.get(), model->bookmark_bar_node(),
children_of_bookmark_bar);
return model;
}
class LocalBookmarkModelMergerTest : public testing::Test {
protected:
LocalBookmarkModelMergerTest() = default;
~LocalBookmarkModelMergerTest() override = default;
base::test::ScopedFeatureList feature_list_{
syncer::kSyncEnableBookmarksInTransportMode};
};
TEST_F(LocalBookmarkModelMergerTest,
ShouldUploadEntireLocalModelIfAccountModelEmpty) {
const std::u16string kFolder1Title = u"folder1";
const std::u16string kFolder2Title = u"folder2";
const std::u16string kUrl1Title = u"url1";
const std::u16string kUrl2Title = u"url2";
const std::u16string kUrl3Title = u"url3";
const std::u16string kUrl4Title = u"url4";
const GURL kUrl1("http://www.url1.com/");
const GURL kUrl2("http://www.url2.com/");
const GURL kUrl3("http://www.url3.com/");
const GURL kUrl4("http://www.url4.com/");
// -------- 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<BookmarkModelView> local_model =
BuildLocalModel({FolderBuilder(kFolder1Title)
.SetChildren({UrlBuilder(kUrl1Title, kUrl1),
UrlBuilder(kUrl2Title, kUrl2)}),
FolderBuilder(kFolder2Title)
.SetChildren({UrlBuilder(kUrl3Title, kUrl3),
UrlBuilder(kUrl4Title, kUrl4)})});
// -------- The account model --------
// bookmark_bar
std::unique_ptr<BookmarkModelView> account_model = BuildAccountModel({});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// -------- The expected merge outcome --------
// Same as the local model described above.
EXPECT_THAT(
account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolder(kFolder1Title,
ElementsAre(IsUrlBookmark(kUrl1Title, kUrl1),
IsUrlBookmark(kUrl2Title, kUrl2))),
IsFolder(kFolder2Title,
ElementsAre(IsUrlBookmark(kUrl3Title, kUrl3),
IsUrlBookmark(kUrl4Title, kUrl4)))));
}
TEST_F(LocalBookmarkModelMergerTest, ShouldIgnoreManagedNodes) {
const std::u16string kUrl1Title = u"url1";
const std::u16string kUrl2Title = u"url2";
const GURL kUrl1("http://www.url1.com/");
const GURL kUrl2("http://www.url2.com/");
auto local_client = std::make_unique<bookmarks::TestBookmarkClient>();
bookmarks::BookmarkNode* local_managed_node =
local_client->EnableManagedNode();
// -------- The local model --------
// bookmark_bar
// |- url1(http://www.url1.com)
// managed_bookmarks
// |- url2(http://www.url2.com)
TestBookmarkModelView local_model(
TestBookmarkModelView::ViewType::kLocalOrSyncableNodes,
std::move(local_client));
FolderBuilder::AddChildrenTo(&local_model, local_model.bookmark_bar_node(),
{UrlBuilder(kUrl1Title, kUrl1)});
FolderBuilder::AddChildrenTo(&local_model, local_managed_node,
{UrlBuilder(kUrl2Title, kUrl2)});
// -------- The account model --------
// bookmark_bar
std::unique_ptr<TestBookmarkModelView> account_model = BuildAccountModel({});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(&local_model, account_model.get()).Merge();
// -------- The expected merge outcome --------
// bookmark_bar
// |- url1(http://www.url1.com)
//
ASSERT_THAT(account_model->bookmark_bar_node()->children(),
ElementsAre(IsUrlBookmark(kUrl1Title, kUrl1)));
// Managed nodes should be excluded from the merge.
EXPECT_THAT(account_model->underlying_model()->GetNodesByURL(kUrl2),
IsEmpty());
}
TEST_F(LocalBookmarkModelMergerTest, ShouldUploadLocalUuid) {
const std::u16string kUrl1Title = u"url1";
const GURL kUrl1("http://www.url1.com/");
const base::Uuid kUrl1Uuid = base::Uuid::GenerateRandomV4();
// -------- The local model --------
// bookmark_bar
// | - bookmark(kUuid/kLocalTitle)
std::unique_ptr<BookmarkModelView> local_model =
BuildLocalModel({UrlBuilder(kUrl1Title, kUrl1).SetUuid(kUrl1Uuid)});
// -------- The account model --------
// bookmark_bar
std::unique_ptr<BookmarkModelView> account_model = BuildAccountModel({});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// -------- The expected merge outcome --------
// Same as the local model described above, including the UUID.
EXPECT_THAT(account_model->bookmark_bar_node()->children(),
ElementsAre(IsUrlBookmarkWithUuid(kUrl1Title, kUrl1, kUrl1Uuid)));
}
TEST_F(LocalBookmarkModelMergerTest, ShouldNotUploadDuplicateBySemantics) {
const std::u16string kFolder1Title = u"folder1";
const std::u16string kUrl1Title = u"url1";
const std::u16string kUrl2Title = u"url2";
const std::u16string kUrl3Title = u"url3";
const GURL kUrl1("http://www.url1.com/");
const GURL kUrl2("http://www.url2.com/");
const GURL kUrl3("http://www.url3.com/");
// -------- The local model --------
// bookmark_bar
// |- folder 1
// |- url1(http://www.url1.com)
// |- url2(http://www.url2.com)
std::unique_ptr<BookmarkModelView> local_model =
BuildLocalModel({FolderBuilder(kFolder1Title)
.SetChildren({UrlBuilder(kUrl1Title, kUrl1),
UrlBuilder(kUrl2Title, kUrl2)})});
// -------- The account model --------
// bookmark_bar
// |- folder 1
// |- url2(http://www.url2.com)
// |- url3(http://www.url3.com)
std::unique_ptr<BookmarkModelView> account_model =
BuildAccountModel({FolderBuilder(kFolder1Title)
.SetChildren({UrlBuilder(kUrl2Title, kUrl2),
UrlBuilder(kUrl3Title, kUrl3)})});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// -------- The expected merge outcome --------
// bookmark_bar
// |- folder 1
// |- url2(http://www.url2.com)
// |- url3(http://www.url3.com)
// |- url1(http://www.url1.com)
EXPECT_THAT(
account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolder(kFolder1Title,
ElementsAre(IsUrlBookmark(kUrl2Title, kUrl2),
IsUrlBookmark(kUrl3Title, kUrl3),
IsUrlBookmark(kUrl1Title, kUrl1)))));
}
TEST_F(LocalBookmarkModelMergerTest, ShouldMergeLocalAndAccountModels) {
const std::u16string kFolder1Title = u"folder1";
const std::u16string kFolder2Title = u"folder2";
const std::u16string kFolder3Title = u"folder3";
const std::u16string kUrl1Title = u"url1";
const std::u16string kUrl2Title = u"url2";
const std::u16string kUrl3Title = u"url3";
const std::u16string kUrl4Title = u"url4";
const GURL kUrl1("http://www.url1.com/");
const GURL kUrl2("http://www.url2.com/");
const GURL kUrl3("http://www.url3.com/");
const GURL kUrl4("http://www.url4.com/");
const GURL kAnotherUrl2("http://www.another-url2.com/");
// -------- 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<BookmarkModelView> local_model =
BuildLocalModel({FolderBuilder(kFolder1Title)
.SetChildren({UrlBuilder(kUrl1Title, kUrl1),
UrlBuilder(kUrl2Title, kUrl2)}),
FolderBuilder(kFolder2Title)
.SetChildren({UrlBuilder(kUrl3Title, kUrl3),
UrlBuilder(kUrl4Title, kUrl4)})});
// -------- The account 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)
std::unique_ptr<BookmarkModelView> account_model = BuildAccountModel(
{FolderBuilder(kFolder1Title)
.SetChildren({UrlBuilder(kUrl1Title, kUrl1),
UrlBuilder(kUrl2Title, kAnotherUrl2)}),
FolderBuilder(kFolder3Title)
.SetChildren({UrlBuilder(kUrl3Title, kUrl3),
UrlBuilder(kUrl4Title, kUrl4)})});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// -------- 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)
EXPECT_THAT(
account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolder(kFolder1Title,
ElementsAre(IsUrlBookmark(kUrl1Title, kUrl1),
IsUrlBookmark(kUrl2Title, kAnotherUrl2),
IsUrlBookmark(kUrl2Title, kUrl2))),
IsFolder(kFolder3Title,
ElementsAre(IsUrlBookmark(kUrl3Title, kUrl3),
IsUrlBookmark(kUrl4Title, kUrl4))),
IsFolder(kFolder2Title,
ElementsAre(IsUrlBookmark(kUrl3Title, kUrl3),
IsUrlBookmark(kUrl4Title, kUrl4)))));
}
// This tests that truncated titles produced by legacy clients are properly
// matched.
TEST_F(LocalBookmarkModelMergerTest,
ShouldMergeLocalAndAccountNodesWhenAccountHasLegacyTruncatedTitle) {
const std::u16string kLocalLongTitle(300, 'A');
const std::u16string kAccountTruncatedTitle(255, 'A');
// -------- The local model --------
std::unique_ptr<BookmarkModelView> local_model =
BuildLocalModel({FolderBuilder(kLocalLongTitle)});
// -------- The account model --------
std::unique_ptr<BookmarkModelView> account_model =
BuildAccountModel({FolderBuilder(kAccountTruncatedTitle)});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// Both titles should have matched against each other.
EXPECT_THAT(account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolder(kLocalLongTitle, IsEmpty())));
}
// This test checks that local node with truncated title will merge with account
// node which has full title.
TEST_F(LocalBookmarkModelMergerTest,
ShouldMergeLocalAndAccountNodesWhenLocalHasLegacyTruncatedTitle) {
const std::u16string kAccountFullTitle(300, 'A');
const std::u16string kLocalTruncatedTitle(255, 'A');
// -------- The local model --------
std::unique_ptr<BookmarkModelView> local_model =
BuildLocalModel({FolderBuilder(kLocalTruncatedTitle)});
// -------- The account model --------
std::unique_ptr<BookmarkModelView> account_model =
BuildAccountModel({FolderBuilder(kAccountFullTitle)});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// Both titles should have matched against each other. Although the local
// title is truncated, for simplicity of the algorithm and considering how
// rare this scenario is, the local one wins.
EXPECT_THAT(account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolder(kLocalTruncatedTitle, IsEmpty())));
}
TEST_F(LocalBookmarkModelMergerTest, ShouldMergeBookmarkByUuid) {
const std::u16string kLocalTitle = u"Title 1";
const std::u16string kAccountTitle = u"Title 2";
const GURL kUrl("http://www.foo.com/");
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
// -------- The local model --------
// bookmark_bar
// | - bookmark(kUuid/kLocalTitle)
std::unique_ptr<BookmarkModelView> local_model =
BuildLocalModel({UrlBuilder(kLocalTitle, kUrl).SetUuid(kUuid)});
// -------- The account model --------
// bookmark_bar
// | - bookmark(kUuid/kAccountTitle)
std::unique_ptr<BookmarkModelView> account_model =
BuildAccountModel({UrlBuilder(kAccountTitle, kUrl).SetUuid(kUuid)});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// -------- The merged model --------
// bookmark_bar
// |- bookmark(kUuid/kLocalTitle)
EXPECT_THAT(account_model->bookmark_bar_node()->children(),
ElementsAre(IsUrlBookmarkWithUuid(kLocalTitle, kUrl, kUuid)));
}
TEST_F(LocalBookmarkModelMergerTest,
ShouldDeduplicateBySemanticsAfterParentMatchedByUuid) {
const std::u16string kFolder1Title = u"folder1";
const std::u16string kFolder2Title = u"folder2";
const std::u16string kFolder3Title = u"folder3";
const std::u16string kUrl1Title = u"url1";
const std::u16string kUrl2Title = u"url2";
const std::u16string kUrl3Title = u"url3";
const GURL kUrl1("http://www.url1.com/");
const GURL kUrl2("http://www.url2.com/");
const GURL kUrl3("http://www.url3.com/");
const base::Uuid kFolder2Uuid = base::Uuid::GenerateRandomV4();
// -------- Local bookmarks --------
// bookmark_bar
// | - folder 1 (kFolder1Title)
// | - folder 2 (kFolder2Uuid/kFolder2Title)
// |- url1(http://www.url1.com)
// |- url2(http://www.url2.com)
std::unique_ptr<BookmarkModelView> local_model =
BuildLocalModel({FolderBuilder(kFolder1Title),
FolderBuilder(kFolder2Title)
.SetUuid(kFolder2Uuid)
.SetChildren({UrlBuilder(kUrl1Title, kUrl1),
UrlBuilder(kUrl2Title, kUrl2)})});
// -------- Account bookmarks --------
// bookmark_bar
// | - folder 3 (kFolder3Title)
// | - folder 2 (kFolder2Uuid/kFolder2Title)
// |- url2(http://www.url1.com)
// |- url3(http://www.url3.com)
std::unique_ptr<BookmarkModelView> account_model = BuildAccountModel(
{FolderBuilder(kFolder3Title)
.SetChildren({FolderBuilder(kFolder2Title)
.SetUuid(kFolder2Uuid)
.SetChildren({UrlBuilder(kUrl2Title, kUrl2),
UrlBuilder(kUrl3Title, kUrl3)})})});
// -------- The expected merge outcome --------
// bookmark_bar
// | - folder 3 (kFolder3Title)
// | - folder 2 (kFolder2Uuid/kTitle2)
// |- url2(http://www.url2.com)
// |- url3(http://www.url3.com)
// |- url1(http://www.url1.com)
// | - folder 1 (kTitle1)
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
EXPECT_THAT(
account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolder(kFolder3Title,
ElementsAre(IsFolder(
kFolder2Title,
ElementsAre(IsUrlBookmark(kUrl2Title, kUrl2),
IsUrlBookmark(kUrl3Title, kUrl3),
IsUrlBookmark(kUrl1Title, kUrl1))))),
IsFolder(kFolder1Title, IsEmpty())));
}
TEST_F(LocalBookmarkModelMergerTest,
ShouldDeduplicateBySemanticsAfterNestedParentMatchedByUuid) {
const std::u16string kFolder1Title = u"folder1";
const std::u16string kFolder2Title = u"folder2";
const std::u16string kUrl1Title = u"url1";
const std::u16string kUrl2Title = u"url2";
const std::u16string kUrl3Title = u"url3";
const GURL kUrl1("http://www.url1.com/");
const GURL kUrl2("http://www.url2.com/");
const GURL kUrl3("http://www.url3.com/");
const base::Uuid kFolder2Uuid = base::Uuid::GenerateRandomV4();
// -------- Local bookmarks --------
// bookmark_bar
// | - folder 1 (kFolder1Title)
// | - folder 2 (kFolder2Uuid/kFolder2Title)
// |- url1(http://www.url1.com)
// |- url2(http://www.url2.com)
std::unique_ptr<BookmarkModelView> local_model = BuildLocalModel(
{FolderBuilder(kFolder1Title)
.SetChildren({FolderBuilder(kFolder2Title)
.SetUuid(kFolder2Uuid)
.SetChildren({UrlBuilder(kUrl1Title, kUrl1),
UrlBuilder(kUrl2Title, kUrl2)})})});
// -------- Account bookmarks --------
// bookmark_bar
// | - folder 2 (kFolder2Uuid/kFolder2Title)
// |- url2(http://www.url2.com)
// |- url3(http://www.url3.com)
std::unique_ptr<BookmarkModelView> account_model =
BuildAccountModel({FolderBuilder(kFolder2Title)
.SetUuid(kFolder2Uuid)
.SetChildren({UrlBuilder(kUrl2Title, kUrl2),
UrlBuilder(kUrl3Title, kUrl3)})});
// -------- The expected merge outcome --------
// bookmark_bar
// | - folder 2 (kFolder2Uuid/kTitle2)
// |- url2(http://www.url2.com)
// |- url3(http://www.url3.com)
// |- url1(http://www.url1.com)
// | - folder 1 (kTitle1)
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
EXPECT_THAT(
account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolder(kFolder2Title,
ElementsAre(IsUrlBookmark(kUrl2Title, kUrl2),
IsUrlBookmark(kUrl3Title, kUrl3),
IsUrlBookmark(kUrl1Title, kUrl1))),
IsFolder(kFolder1Title, IsEmpty())));
}
TEST_F(LocalBookmarkModelMergerTest,
ShouldDeduplicateBySemanticsAfterTwoConsecutiveAncestorsMatchedByUuid) {
const std::u16string kFolder1Title = u"folder1";
const std::u16string kFolder2Title = u"folder2";
const std::u16string kFolder3Title = u"folder3";
const std::u16string kUrl1Title = u"url1";
const std::u16string kUrl2Title = u"url2";
const std::u16string kUrl3Title = u"url3";
const GURL kUrl1("http://www.url1.com/");
const GURL kUrl2("http://www.url2.com/");
const GURL kUrl3("http://www.url3.com/");
const base::Uuid kFolder2Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kFolder3Uuid = base::Uuid::GenerateRandomV4();
// -------- Local bookmarks --------
// bookmark_bar
// | - folder 1 (kFolder1Title)
// | - folder 2 (kFolder2Uuid/kFolder2Title)
// | - folder 3 (kFolder3Uuid/kFolder3Title)
// |- url1(http://www.url1.com)
// |- url2(http://www.url2.com)
std::unique_ptr<BookmarkModelView> local_model = BuildLocalModel(
{FolderBuilder(kFolder1Title)
.SetChildren(
{FolderBuilder(kFolder2Title)
.SetUuid(kFolder2Uuid)
.SetChildren(
{FolderBuilder(kFolder3Title)
.SetUuid(kFolder3Uuid)
.SetChildren(
{UrlBuilder(kUrl1Title, kUrl1),
UrlBuilder(kUrl2Title, kUrl2)})})})});
// -------- Account bookmarks --------
// bookmark_bar
// | - folder 2 (kFolder2Uuid/kFolder2Title)
// | - folder 3 (kFolder3Uuid/kFolder3Title)
// |- url2(http://www.url2.com)
// |- url3(http://www.url3.com)
std::unique_ptr<BookmarkModelView> account_model = BuildAccountModel(
{FolderBuilder(kFolder2Title)
.SetUuid(kFolder2Uuid)
.SetChildren({FolderBuilder(kFolder3Title)
.SetUuid(kFolder3Uuid)
.SetChildren({UrlBuilder(kUrl2Title, kUrl2),
UrlBuilder(kUrl3Title, kUrl3)})})});
// -------- The expected merge outcome --------
// bookmark_bar
// | - folder 2 (kFolder2Uuid/kTitle2)
// | - folder 3 (kFolder3Uuid/kTitle3)
// |- url2(http://www.url2.com)
// |- url3(http://www.url3.com)
// |- url1(http://www.url1.com)
// | - folder 1 (kTitle1)
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
EXPECT_THAT(
account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolder(kFolder2Title,
ElementsAre(IsFolder(
kFolder3Title,
ElementsAre(IsUrlBookmark(kUrl2Title, kUrl2),
IsUrlBookmark(kUrl3Title, kUrl3),
IsUrlBookmark(kUrl1Title, kUrl1))))),
IsFolder(kFolder1Title, IsEmpty())));
}
TEST_F(
LocalBookmarkModelMergerTest,
ShouldDeduplicateBySemanticsAfterTwoNonConsecutiveAncestorsMatchedByUuid) {
const std::u16string kFolder1Title = u"folder1";
const std::u16string kFolder2Title = u"folder2";
const std::u16string kFolder3Title = u"folder3";
const std::u16string kFolder4Title = u"folder4";
const std::u16string kFolder5Title = u"folder5";
const std::u16string kUrl1Title = u"url1";
const std::u16string kUrl2Title = u"url2";
const std::u16string kUrl3Title = u"url3";
const GURL kUrl1("http://www.url1.com/");
const GURL kUrl2("http://www.url2.com/");
const GURL kUrl3("http://www.url3.com/");
const base::Uuid kFolder2Uuid = base::Uuid::GenerateRandomV4();
const base::Uuid kFolder5Uuid = base::Uuid::GenerateRandomV4();
// -------- Local bookmarks --------
// bookmark_bar
// | - folder 1 (kFolder1Title)
// | - folder 2 (kFolder2Uuid/kFolder2Title)
// | - folder 3 (kFolder3Title)
// | - folder 4 (kFolder4Title)
// | - folder 5 (kFolder5Uuid/kFolder5Title)
// |- url1(http://www.url1.com)
// |- url2(http://www.url2.com)
std::unique_ptr<BookmarkModelView> local_model = BuildLocalModel(
{FolderBuilder(kFolder1Title)
.SetChildren(
{FolderBuilder(kFolder2Title)
.SetUuid(kFolder2Uuid)
.SetChildren(
{FolderBuilder(kFolder3Title)
.SetChildren(
{FolderBuilder(kFolder4Title)
.SetChildren(
{FolderBuilder(kFolder5Title)
.SetUuid(kFolder5Uuid)
.SetChildren(
{UrlBuilder(kUrl1Title,
kUrl1),
UrlBuilder(
kUrl2Title,
kUrl2)})})})})})});
// -------- Account bookmarks --------
// bookmark_bar
// | - folder 2 (kFolder2Uuid/kFolder2Title)
// | - folder 5 (kFolder5Uuid/kFolder5Title)
// |- url2(http://www.url2.com)
// |- url3(http://www.url3.com)
std::unique_ptr<BookmarkModelView> account_model =
BuildAccountModel({FolderBuilder(kFolder2Title).SetUuid(kFolder2Uuid),
FolderBuilder(kFolder5Title)
.SetUuid(kFolder5Uuid)
.SetChildren({UrlBuilder(kUrl2Title, kUrl2),
UrlBuilder(kUrl3Title, kUrl3)})});
// -------- The expected merge outcome --------
// bookmark_bar
// | - folder 2 (kFolder2Uuid/kTitle2)
// | - folder 3 (kTitle3)
// | - folder 4 (kTitle4)
// | - folder 5 (kFolder5Uuid/kFolder5Title)
// |- url2(http://www.url2.com)
// |- url3(http://www.url3.com)
// |- url1(http://www.url1.com)
// | - folder 1 (kTitle1)
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
EXPECT_THAT(
account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolder(kFolder2Title,
ElementsAre(IsFolder(
kFolder3Title, ElementsAre(IsFolder(
kFolder4Title, IsEmpty()))))),
IsFolder(kFolder5Title,
ElementsAre(IsUrlBookmark(kUrl2Title, kUrl2),
IsUrlBookmark(kUrl3Title, kUrl3),
IsUrlBookmark(kUrl1Title, kUrl1))),
IsFolder(kFolder1Title, IsEmpty())));
}
TEST_F(LocalBookmarkModelMergerTest,
ShouldMergeBookmarkByUuidDespiteDifferentParent) {
const std::u16string kFolderTitle = u"Folder Title";
const std::u16string kLocalTitle = u"Title 1";
const std::u16string kAccountTitle = u"Title 2";
const GURL kUrl("http://www.foo.com/");
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
// -------- The local model --------
// bookmark_bar
// |- bookmark(kUuid/kLocalTitle)
std::unique_ptr<BookmarkModelView> local_model =
BuildLocalModel({UrlBuilder(kLocalTitle, kUrl).SetUuid(kUuid)});
// -------- The account model --------
// bookmark_bar
// | - folder
// | - bookmark(kUuid/kAccountTitle)
std::unique_ptr<BookmarkModelView> account_model = BuildAccountModel(
{FolderBuilder(kFolderTitle)
.SetChildren({UrlBuilder(kAccountTitle, kUrl).SetUuid(kUuid)})});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// -------- The merged model --------
// bookmark_bar
// | - folder
// | - bookmark(kUuid/kLocalTitle)
EXPECT_THAT(
account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolder(kFolderTitle, ElementsAre(IsUrlBookmarkWithUuid(
kLocalTitle, kUrl, kUuid)))));
}
TEST_F(LocalBookmarkModelMergerTest,
ShouldNotMergeBySemanticsIfDifferentParent) {
const std::u16string kFolder1Title = u"folder1";
const std::u16string kFolder2Title = u"folder2";
const std::u16string kUrl1Title = u"url1";
const std::u16string kUrl2Title = u"url2";
const GURL kUrl1("http://www.url1.com/");
const GURL kUrl2("http://www.url2.com/");
// -------- The local model --------
// bookmark_bar
// |- folder 1
// |- folder 2
// |- url1(http://www.url1.com)
std::unique_ptr<BookmarkModelView> local_model = BuildLocalModel(
{FolderBuilder(kFolder1Title)
.SetChildren({FolderBuilder(kFolder2Title)
.SetChildren({UrlBuilder(kUrl1Title, kUrl1)})})});
// -------- The account model --------
// bookmark_bar
// |- folder 2
// |- url2(http://www.url2.com)
std::unique_ptr<BookmarkModelView> account_model =
BuildAccountModel({FolderBuilder(kFolder2Title)
.SetChildren({UrlBuilder(kUrl2Title, kUrl2)})});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// -------- The merged model --------
// bookmark_bar
// |- folder 2
// |- url2(http://www.url2.com)
// |- folder 1
// |- folder 2
// |- url1(http://www.url1.com)
EXPECT_THAT(
account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolder(kFolder2Title,
ElementsAre(IsUrlBookmark(kUrl2Title, kUrl2))),
IsFolder(kFolder1Title,
ElementsAre(IsFolder(kFolder2Title,
ElementsAre(IsUrlBookmark(
kUrl1Title, kUrl1)))))));
}
TEST_F(LocalBookmarkModelMergerTest, ShouldMergeFolderByUuidAndNotSemantics) {
const std::u16string kTitle1 = u"Title 1";
const std::u16string kTitle2 = u"Title 2";
const GURL kUrl("http://www.foo.com/");
const base::Uuid kUuid1 = base::Uuid::GenerateRandomV4();
const base::Uuid kUuid2 = base::Uuid::GenerateRandomV4();
// -------- The local model --------
// bookmark_bar
// | - folder 1 (kUuid1/kTitle1)
// | - folder 2 (kUuid2/kTitle2)
std::unique_ptr<BookmarkModelView> local_model =
BuildLocalModel({FolderBuilder(kTitle1).SetUuid(kUuid1).SetChildren(
{FolderBuilder(kTitle2).SetUuid(kUuid2)})});
// -------- The account model --------
// bookmark_bar
// | - folder (kUuid2/kTitle1)
std::unique_ptr<BookmarkModelView> account_model =
BuildAccountModel({FolderBuilder(kTitle1).SetUuid(kUuid2)});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// -------- The merged model --------
// bookmark_bar
// | - folder 2 (kUuid2/kTitle2)
// | - folder 1 (kUuid1/kTitle1)
//
// The node should have been merged with its UUID match, even if the other
// candidate matches by semantics.
EXPECT_THAT(account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolderWithUuid(kTitle2, kUuid2, IsEmpty()),
IsFolderWithUuid(kTitle1, kUuid1, IsEmpty())));
}
TEST_F(
LocalBookmarkModelMergerTest,
ShouldIgnoreFolderSemanticsMatchAndLaterMatchByUuidWithSemanticsNodeFirst) {
const std::u16string kLocalOnlyTitle = u"LocalOnlyTitle";
const std::u16string kMatchingTitle = u"MatchingTitle";
const base::Uuid kUuid1 = base::Uuid::GenerateRandomV4();
const base::Uuid kUuid2 = base::Uuid::GenerateRandomV4();
const GURL kUrl("http://foo.com/");
const std::u16string kUrlTitle = u"Bookmark Title";
// -------- The local model --------
// bookmark_bar
// | - folder (kUuid1/kMatchingTitle)
// | - folder (kUuid2/kLocalOnlyTitle)
// | - bookmark
std::unique_ptr<BookmarkModelView> local_model =
BuildLocalModel({FolderBuilder(kMatchingTitle).SetUuid(kUuid1),
FolderBuilder(kLocalOnlyTitle)
.SetUuid(kUuid2)
.SetChildren({UrlBuilder(kUrlTitle, kUrl)})});
// -------- The account model --------
// bookmark_bar
// | - folder (kUuid2/kMatchingTitle)
std::unique_ptr<BookmarkModelView> account_model =
BuildAccountModel({FolderBuilder(kMatchingTitle).SetUuid(kUuid2)});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// -------- The merged model --------
// bookmark_bar
// | - folder (kUuid2/kLocalOnlyTitle)
// | - bookmark
// | - folder (kUuid1/kMatchingTitle)
//
// The node should have been merged with its UUID match, even if the other
// candidate matches by semantics.
EXPECT_THAT(
account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolderWithUuid(kLocalOnlyTitle, kUuid2,
ElementsAre(IsUrlBookmark(kUrlTitle, kUrl))),
IsFolderWithUuid(kMatchingTitle, kUuid1, IsEmpty())));
}
TEST_F(LocalBookmarkModelMergerTest,
ShouldIgnoreFolderSemanticsMatchAndLaterMatchByUuidWithUuidNodeFirst) {
const std::u16string kLocalOnlyTitle = u"LocalOnlyTitle";
const std::u16string kMatchingTitle = u"MatchingTitle";
const base::Uuid kUuid1 = base::Uuid::GenerateRandomV4();
const base::Uuid kUuid2 = base::Uuid::GenerateRandomV4();
const GURL kUrl("http://foo.com/");
const std::u16string kUrlTitle = u"Bookmark Title";
// -------- The local model --------
// bookmark_bar
// | - folder (kUuid2/kLocalOnlyTitle)
// | - bookmark
// | - folder (kUuid1/kMatchingTitle)
std::unique_ptr<BookmarkModelView> local_model =
BuildLocalModel({FolderBuilder(kLocalOnlyTitle)
.SetUuid(kUuid2)
.SetChildren({UrlBuilder(kUrlTitle, kUrl)}),
FolderBuilder(kMatchingTitle).SetUuid(kUuid1)});
// -------- The account model --------
// bookmark_bar
// | - folder (kUuid2/kMatchingTitle)
std::unique_ptr<BookmarkModelView> account_model =
BuildAccountModel({FolderBuilder(kMatchingTitle).SetUuid(kUuid2)});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// -------- The merged model --------
// bookmark_bar
// | - folder (kUuid2/kLocalOnlyTitle)
// | - bookmark
// | - folder (kUuid1/kMatchingTitle)
//
// The node should have been merged with its UUID match, even if the other
// candidate matches by semantics.
EXPECT_THAT(
account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolderWithUuid(kLocalOnlyTitle, kUuid2,
ElementsAre(IsUrlBookmark(kUrlTitle, kUrl))),
IsFolderWithUuid(kMatchingTitle, kUuid1, IsEmpty())));
}
TEST_F(LocalBookmarkModelMergerTest,
ShouldReplaceBookmarkUuidWithConflictingURLs) {
const std::u16string kTitle = u"Title";
const GURL kUrl1("http://www.foo.com/");
const GURL kUrl2("http://www.bar.com/");
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
// -------- The local model --------
// bookmark_bar
// | - bookmark (kUuid/kUrl1)
std::unique_ptr<BookmarkModelView> local_model =
BuildLocalModel({UrlBuilder(kTitle, kUrl1).SetUuid(kUuid)});
// -------- The account model --------
// bookmark_bar
// | - bookmark (kUuid/kUrl2)
std::unique_ptr<BookmarkModelView> account_model =
BuildAccountModel({UrlBuilder(kTitle, kUrl2).SetUuid(kUuid)});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// -------- The merged model --------
// bookmark_bar
// | - bookmark (kUuid/kUrl2)
// | - bookmark ([new UUID]/kUrl1)
//
// The conflicting node UUID should have been replaced.
EXPECT_THAT(account_model->bookmark_bar_node()->children(),
ElementsAre(IsUrlBookmarkWithUuid(kTitle, kUrl2, kUuid),
IsUrlBookmarkWithUuid(kTitle, kUrl1, Ne(kUuid))));
}
TEST_F(LocalBookmarkModelMergerTest,
ShouldReplaceBookmarkUuidWithConflictingTypes) {
const GURL kUrl1("http://www.foo.com/");
const std::u16string kTitle = u"Title";
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
// -------- The local model --------
// bookmark_bar
// | - bookmark (kUuid/kUrl1)
std::unique_ptr<BookmarkModelView> local_model =
BuildLocalModel({UrlBuilder(kTitle, kUrl1).SetUuid(kUuid)});
// -------- The account model --------
// bookmark_bar
// | - folder(kUuid)
std::unique_ptr<BookmarkModelView> account_model =
BuildAccountModel({FolderBuilder(kTitle).SetUuid(kUuid)});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// -------- The merged model --------
// bookmark_bar
// | - folder (kUuid)
// | - bookmark ([new UUID])
//
// The conflicting node UUID should have been replaced.
EXPECT_THAT(account_model->bookmark_bar_node()->children(),
ElementsAre(IsFolderWithUuid(kTitle, kUuid, IsEmpty()),
IsUrlBookmarkWithUuid(kTitle, kUrl1, Ne(kUuid))));
}
TEST_F(LocalBookmarkModelMergerTest,
ShouldReplaceBookmarkUuidWithConflictingTypesAndLocalChildren) {
const std::u16string kFolderTitle = u"Folder Title";
const std::u16string kUrl1Title = u"url1";
const std::u16string kUrl2Title = u"url2";
const GURL kUrl1("http://www.url1.com/");
const GURL kUrl2("http://www.url2.com/");
const base::Uuid kUuid = base::Uuid::GenerateRandomV4();
// -------- The local model --------
// bookmark_bar
// | - folder (kUuid)
// | - bookmark (kUrl1)
std::unique_ptr<BookmarkModelView> local_model =
BuildLocalModel({FolderBuilder(kFolderTitle)
.SetUuid(kUuid)
.SetChildren({UrlBuilder(kUrl1Title, kUrl1)})});
// -------- The account model --------
// bookmark_bar
// | - bookmark (kUuid/kUrl2)
std::unique_ptr<BookmarkModelView> account_model =
BuildAccountModel({UrlBuilder(kUrl2Title, kUrl2).SetUuid(kUuid)});
// -------- Exercise the merge logic --------
LocalBookmarkModelMerger(local_model.get(), account_model.get()).Merge();
// -------- The merged model --------
// bookmark_bar
// | - bookmark (kUuid/kUrl2)
// | - folder ([new UUID])
// | - bookmark (kUrl1)
//
// The conflicting node UUID should have been replaced.
EXPECT_THAT(account_model->bookmark_bar_node()->children(),
ElementsAre(IsUrlBookmarkWithUuid(kUrl2Title, kUrl2, kUuid),
IsFolderWithUuid(
kFolderTitle, Ne(kUuid),
ElementsAre(IsUrlBookmark(kUrl1Title, kUrl1)))));
}
} // namespace
} // namespace sync_bookmarks