| // 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 |