blob: 6a6beb2ea5308670c4a637b54389abef6d554c0f [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/sync_bookmarks/bookmark_local_data_batch_uploader.h"
#include <memory>
#include <utility>
#include <variant>
#include "base/strings/utf_ostream_operators.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/common/bookmark_pref_names.h"
#include "components/bookmarks/test/test_bookmark_client.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/testing_pref_service.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/strings/grit/components_strings.h"
#include "components/sync/service/local_data_description.h"
#include "components/sync/test/test_matchers.h"
#include "components/sync_bookmarks/switches.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"
namespace sync_bookmarks {
namespace {
using ::syncer::IsEmptyLocalDataDescription;
using ::syncer::MatchesLocalDataDescription;
using ::syncer::MatchesLocalDataItemModel;
using ::testing::_;
using ::testing::ElementsAre;
using ::testing::IsEmpty;
MATCHER_P2(MatchesTitleAndUrl, title, url, "") {
if (!arg->is_url()) {
*result_listener << "Expected URL bookmark but got folder.";
return false;
}
if (arg->GetTitle() != title) {
*result_listener << "Expected URL title \"" << title << "\" but got \""
<< arg->GetTitle() << "\"";
return false;
}
if (arg->url() != url) {
*result_listener << "Expected URL \"" << url << "\" but got \""
<< arg->url() << "\"";
return false;
}
return true;
}
class BookmarkLocalDataBatchUploaderTest : public ::testing::Test {
public:
BookmarkLocalDataBatchUploaderTest() {
pref_service_.registry()->RegisterBooleanPref(
bookmarks::prefs::kEditBookmarksEnabled, true);
}
~BookmarkLocalDataBatchUploaderTest() override = default;
bookmarks::BookmarkModel* bookmark_model() { return bookmark_model_.get(); }
PrefService* pref_service() { return &pref_service_; }
private:
base::test::ScopedFeatureList feature_list_{
switches::kSyncEnableBookmarksInTransportMode};
const std::unique_ptr<bookmarks::BookmarkModel> bookmark_model_ =
std::make_unique<bookmarks::BookmarkModel>(
std::make_unique<bookmarks::TestBookmarkClient>());
TestingPrefServiceSimple pref_service_;
};
TEST_F(BookmarkLocalDataBatchUploaderTest, LocalDescriptionEmptyIfNullModel) {
BookmarkLocalDataBatchUploader uploader(nullptr, pref_service());
base::test::TestFuture<syncer::LocalDataDescription> description;
uploader.GetLocalDataDescription(description.GetCallback());
EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
}
TEST_F(BookmarkLocalDataBatchUploaderTest,
LocalDescriptionEmptyIfModelNotLoaded) {
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
base::test::TestFuture<syncer::LocalDataDescription> description;
uploader.GetLocalDataDescription(description.GetCallback());
EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
}
TEST_F(BookmarkLocalDataBatchUploaderTest,
LocalDescriptionEmptyIfTransportModeOff) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(
switches::kSyncEnableBookmarksInTransportMode);
bookmark_model()->LoadEmptyForTest();
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
base::test::TestFuture<syncer::LocalDataDescription> description;
uploader.GetLocalDataDescription(description.GetCallback());
EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
}
TEST_F(BookmarkLocalDataBatchUploaderTest,
LocalDescriptionEmptyIfNoAccountFolders) {
bookmark_model()->LoadEmptyForTest();
ASSERT_FALSE(bookmark_model()->account_bookmark_bar_node());
bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(), /*index=*/0,
u"Local", GURL("http://local.com/"));
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
base::test::TestFuture<syncer::LocalDataDescription> description;
uploader.GetLocalDataDescription(description.GetCallback());
EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
}
TEST_F(BookmarkLocalDataBatchUploaderTest,
LocalDescriptionEmptyIfEditBookmarksDislabed) {
pref_service()->SetBoolean(bookmarks::prefs::kEditBookmarksEnabled, false);
bookmark_model()->LoadEmptyForTest();
bookmark_model()->CreateAccountPermanentFolders();
bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(), /*index=*/0,
u"Local", GURL("http://local.com/"));
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
base::test::TestFuture<syncer::LocalDataDescription> description;
uploader.GetLocalDataDescription(description.GetCallback());
EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
}
TEST_F(BookmarkLocalDataBatchUploaderTest, LocalDescriptionOnlyHasLocalData) {
bookmark_model()->LoadEmptyForTest();
bookmark_model()->CreateAccountPermanentFolders();
const bookmarks::BookmarkNode* local_node = bookmark_model()->AddURL(
bookmark_model()->bookmark_bar_node(), /*index=*/0, u"Local",
GURL("http://local.com/"));
bookmark_model()->AddURL(bookmark_model()->account_bookmark_bar_node(),
/*index=*/0, u"Account",
GURL("http://account.com/"));
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
base::test::TestFuture<syncer::LocalDataDescription> description;
uploader.GetLocalDataDescription(description.GetCallback());
EXPECT_THAT(description.Get(),
MatchesLocalDataDescription(
syncer::DataType::BOOKMARKS,
ElementsAre(MatchesLocalDataItemModel(
local_node->id(),
syncer::LocalDataItemModel::PageUrlIcon(
GURL("http://local.com/")),
/*title=*/"Local", /*subtitle=*/IsEmpty())),
/*item_count=*/1u, /*domains=*/ElementsAre("local.com"),
/*domain_count=*/1u));
}
TEST_F(BookmarkLocalDataBatchUploaderTest,
LocalDescriptionEmptyItemsWhenSelectedItemsFeatureDisabled) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(
switches::kSyncBookmarksBatchUploadSelectedItems);
bookmark_model()->LoadEmptyForTest();
bookmark_model()->CreateAccountPermanentFolders();
bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(), /*index=*/0,
u"Local", GURL("http://local.com/"));
bookmark_model()->AddURL(bookmark_model()->account_bookmark_bar_node(),
/*index=*/0, u"Account",
GURL("http://account.com/"));
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
base::test::TestFuture<syncer::LocalDataDescription> description;
uploader.GetLocalDataDescription(description.GetCallback());
EXPECT_THAT(description.Get(),
MatchesLocalDataDescription(_, /*local_data_models=*/IsEmpty(),
/*item_count=*/1u,
/*domains=*/ElementsAre("local.com"),
/*domain_count=*/1u));
}
TEST_F(BookmarkLocalDataBatchUploaderTest,
LocalDescriptionTopLevelEmptyFolder) {
bookmark_model()->LoadEmptyForTest();
bookmark_model()->CreateAccountPermanentFolders();
const bookmarks::BookmarkNode* folder = bookmark_model()->AddFolder(
bookmark_model()->bookmark_bar_node(), /*index=*/0, u"folder");
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
base::test::TestFuture<syncer::LocalDataDescription> description;
uploader.GetLocalDataDescription(description.GetCallback());
EXPECT_THAT(description.Get(),
MatchesLocalDataDescription(
syncer::DataType::BOOKMARKS,
ElementsAre(MatchesLocalDataItemModel(
folder->id(), syncer::LocalDataItemModel::FolderIcon(),
/*title=*/"folder",
/*subtitle=*/"0 bookmarks")),
/*item_count=*/0u, /*domains=*/IsEmpty(),
/*domain_count=*/0u));
}
TEST_F(BookmarkLocalDataBatchUploaderTest, LocalDescriptionFolderNesting) {
bookmark_model()->LoadEmptyForTest();
bookmark_model()->CreateAccountPermanentFolders();
// Create the following structure:
// bookmark_bar
// l1_folder
// l2_folder
// l3_url
// l2_url
// l1_url
const bookmarks::BookmarkNode* l1_folder = bookmark_model()->AddFolder(
bookmark_model()->bookmark_bar_node(), /*index=*/0, u"l1_folder");
const bookmarks::BookmarkNode* l2_folder =
bookmark_model()->AddFolder(l1_folder, /*index=*/0, u"l2_folder");
bookmark_model()->AddURL(l2_folder, /*index=*/0, u"l3_url",
GURL("http://l3.com/"));
bookmark_model()->AddURL(l1_folder, /*index=*/1, u"l2_url",
GURL("http://l2.com/"));
const bookmarks::BookmarkNode* l1_bookmark =
bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
/*index=*/1, u"l1_url", GURL("http://l1.com/"));
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
base::test::TestFuture<syncer::LocalDataDescription> description;
uploader.GetLocalDataDescription(description.GetCallback());
EXPECT_THAT(
description.Get(),
MatchesLocalDataDescription(
syncer::DataType::BOOKMARKS,
// The full list includes only the top-level items. The bookmark count
// includes the URLs in the subtree (but not the folder).
ElementsAre(MatchesLocalDataItemModel(
l1_folder->id(),
syncer::LocalDataItemModel::FolderIcon(), "l1_folder",
l10n_util::GetPluralStringFUTF8(
IDS_BULK_UPLOAD_BOOKMARK_FOLDER_SUBTITLE, 2)),
MatchesLocalDataItemModel(
l1_bookmark->id(),
syncer::LocalDataItemModel::PageUrlIcon(
GURL("http://l1.com/")),
/*title=*/"l1_url", /*subtitle=*/IsEmpty())),
/*item_count=*/3u,
/*domains=*/ElementsAre("l1.com", "l2.com", "l3.com"),
/*domain_count=*/3u));
}
TEST_F(BookmarkLocalDataBatchUploaderTest, LocalDescriptionHasSortedDomains) {
bookmark_model()->LoadEmptyForTest();
bookmark_model()->CreateAccountPermanentFolders();
const bookmarks::BookmarkNode* bookmark_a =
bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
/*index=*/0, u"Z", GURL("https://a.com"));
const bookmarks::BookmarkNode* bookmark_b =
bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
/*index=*/0, u"A", GURL("http://b.com"));
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
base::test::TestFuture<syncer::LocalDataDescription> description;
uploader.GetLocalDataDescription(description.GetCallback());
EXPECT_THAT(
description.Get(),
MatchesLocalDataDescription(
syncer::DataType::BOOKMARKS,
// Ordered by recency.
ElementsAre(
MatchesLocalDataItemModel(
bookmark_b->id(),
syncer::LocalDataItemModel::PageUrlIcon(GURL("http://b.com")),
/*title=*/"A", /*subtitle=*/IsEmpty()),
MatchesLocalDataItemModel(bookmark_a->id(),
syncer::LocalDataItemModel::PageUrlIcon(
GURL("https://a.com")),
/*title=*/"Z", /*subtitle=*/IsEmpty())),
/*item_count=*/2u,
// Sorting is *not* by bookmark name, nor by full URL (http://b.com is
// < https://a.com). It's by domain (a.com < b.com).
/*domains=*/ElementsAre("a.com", "b.com"),
/*domain_count=*/2u));
}
TEST_F(BookmarkLocalDataBatchUploaderTest, LocalDescriptionHasNoManagedUrls) {
// Create a new model with the managed node enabled.
auto client = std::make_unique<bookmarks::TestBookmarkClient>();
bookmarks::BookmarkNode* managed_node = client->EnableManagedNode();
auto model = std::make_unique<bookmarks::BookmarkModel>(std::move(client));
model->LoadEmptyForTest();
model->CreateAccountPermanentFolders();
model->AddURL(managed_node, /*index=*/0, u"Managed",
GURL("http://managed.com"));
BookmarkLocalDataBatchUploader uploader(model.get(), pref_service());
base::test::TestFuture<syncer::LocalDataDescription> description;
uploader.GetLocalDataDescription(description.GetCallback());
EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
}
TEST_F(BookmarkLocalDataBatchUploaderTest, MigrationNoOpsIfNullModel) {
BookmarkLocalDataBatchUploader uploader(nullptr, pref_service());
uploader.TriggerLocalDataMigration();
// Should not crash.
}
TEST_F(BookmarkLocalDataBatchUploaderTest, MigrationNoOpsIfModelNotLoaded) {
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
uploader.TriggerLocalDataMigration();
// Should not crash.
}
TEST_F(BookmarkLocalDataBatchUploaderTest, MigrationNoOpsIfTransportModeOff) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(
switches::kSyncEnableBookmarksInTransportMode);
bookmark_model()->LoadEmptyForTest();
bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
/*index=*/0, u"Local", GURL("http://local.com/"));
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
uploader.TriggerLocalDataMigration();
EXPECT_THAT(bookmark_model()->bookmark_bar_node()->children(),
ElementsAre(MatchesTitleAndUrl(u"Local", "http://local.com/")));
EXPECT_THAT(bookmark_model()->mobile_node()->children(), IsEmpty());
EXPECT_THAT(bookmark_model()->other_node()->children(), IsEmpty());
}
TEST_F(BookmarkLocalDataBatchUploaderTest,
MigrationNoOpsIfAccountNodesMissing) {
bookmark_model()->LoadEmptyForTest();
ASSERT_FALSE(bookmark_model()->account_bookmark_bar_node());
bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
/*index=*/0, u"Local", GURL("http://local.com/"));
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
uploader.TriggerLocalDataMigration();
EXPECT_THAT(bookmark_model()->bookmark_bar_node()->children(),
ElementsAre(MatchesTitleAndUrl(u"Local", "http://local.com/")));
EXPECT_THAT(bookmark_model()->mobile_node()->children(), IsEmpty());
EXPECT_THAT(bookmark_model()->other_node()->children(), IsEmpty());
}
TEST_F(BookmarkLocalDataBatchUploaderTest,
FullMigrationNoOpsIfEditBookmarksDisalbed) {
pref_service()->SetBoolean(bookmarks::prefs::kEditBookmarksEnabled, false);
bookmark_model()->LoadEmptyForTest();
bookmark_model()->CreateAccountPermanentFolders();
bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
/*index=*/0, u"Local", GURL("http://local.com/"));
ASSERT_THAT(bookmark_model()->account_bookmark_bar_node()->children(),
IsEmpty());
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
uploader.TriggerLocalDataMigration();
EXPECT_THAT(bookmark_model()->bookmark_bar_node()->children(),
ElementsAre(MatchesTitleAndUrl(u"Local", "http://local.com/")));
EXPECT_THAT(bookmark_model()->mobile_node()->children(), IsEmpty());
EXPECT_THAT(bookmark_model()->other_node()->children(), IsEmpty());
EXPECT_THAT(bookmark_model()->account_bookmark_bar_node()->children(),
IsEmpty());
}
TEST_F(BookmarkLocalDataBatchUploaderTest,
PartialMigrationNoOpsIfEditBookmarksDisalbed) {
pref_service()->SetBoolean(bookmarks::prefs::kEditBookmarksEnabled, false);
bookmark_model()->LoadEmptyForTest();
bookmark_model()->CreateAccountPermanentFolders();
const bookmarks::BookmarkNode* local_node = bookmark_model()->AddURL(
bookmark_model()->bookmark_bar_node(),
/*index=*/0, u"Local", GURL("http://local.com/"));
ASSERT_THAT(bookmark_model()->account_bookmark_bar_node()->children(),
IsEmpty());
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
uploader.TriggerLocalDataMigrationForItems(
{syncer::LocalDataItemModel::DataId(local_node->id())});
EXPECT_THAT(bookmark_model()->bookmark_bar_node()->children(),
ElementsAre(MatchesTitleAndUrl(u"Local", "http://local.com/")));
EXPECT_THAT(bookmark_model()->mobile_node()->children(), IsEmpty());
EXPECT_THAT(bookmark_model()->other_node()->children(), IsEmpty());
EXPECT_THAT(bookmark_model()->account_bookmark_bar_node()->children(),
IsEmpty());
}
// Note: Most of the merging logic is verified in the unit tests for
// LocalBookmarkModelMerger, this test only checks the communication between the
// 2 layers.
TEST_F(BookmarkLocalDataBatchUploaderTest, FullMigrationUploadsLocalBookmarks) {
bookmark_model()->LoadEmptyForTest();
bookmark_model()->CreateAccountPermanentFolders();
bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
/*index=*/0, u"Local", GURL("http://local.com/"));
bookmark_model()->AddURL(bookmark_model()->account_bookmark_bar_node(),
/*index=*/0, u"Account",
GURL("http://account.com/"));
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
uploader.TriggerLocalDataMigration();
EXPECT_THAT(bookmark_model()->bookmark_bar_node()->children(), IsEmpty());
EXPECT_THAT(bookmark_model()->mobile_node()->children(), IsEmpty());
EXPECT_THAT(bookmark_model()->other_node()->children(), IsEmpty());
EXPECT_THAT(bookmark_model()->account_bookmark_bar_node()->children(),
ElementsAre(MatchesTitleAndUrl(u"Account", "http://account.com/"),
MatchesTitleAndUrl(u"Local", "http://local.com/")));
EXPECT_THAT(bookmark_model()->account_mobile_node()->children(), IsEmpty());
EXPECT_THAT(bookmark_model()->account_other_node()->children(), IsEmpty());
}
// Note: Most of the merging logic is verified in the unit tests for
// LocalBookmarkModelMerger, this test only checks the communication between the
// 2 layers.
TEST_F(BookmarkLocalDataBatchUploaderTest,
PartialMigrationUploadsSelectedLocalBookmarks) {
bookmark_model()->LoadEmptyForTest();
bookmark_model()->CreateAccountPermanentFolders();
const bookmarks::BookmarkNode* local_node1 = bookmark_model()->AddURL(
bookmark_model()->bookmark_bar_node(),
/*index=*/0, u"Local", GURL("http://local1.com/"));
bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
/*index=*/1, u"Local", GURL("http://local2.com/"));
bookmark_model()->AddURL(bookmark_model()->account_bookmark_bar_node(),
/*index=*/0, u"Account",
GURL("http://account.com/"));
BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
uploader.TriggerLocalDataMigrationForItems(
{syncer::LocalDataItemModel::DataId(local_node1->id())});
EXPECT_THAT(bookmark_model()->bookmark_bar_node()->children(),
ElementsAre(MatchesTitleAndUrl(u"Local", "http://local2.com/")));
EXPECT_THAT(bookmark_model()->mobile_node()->children(), IsEmpty());
EXPECT_THAT(bookmark_model()->other_node()->children(), IsEmpty());
EXPECT_THAT(bookmark_model()->account_bookmark_bar_node()->children(),
ElementsAre(MatchesTitleAndUrl(u"Account", "http://account.com/"),
MatchesTitleAndUrl(u"Local", "http://local1.com/")));
EXPECT_THAT(bookmark_model()->account_mobile_node()->children(), IsEmpty());
EXPECT_THAT(bookmark_model()->account_other_node()->children(), IsEmpty());
}
} // namespace
} // namespace sync_bookmarks