chromium/ios/chrome/browser/bookmarks/ui_bundled/bookmark_mediator_unittest.mm

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

#import "ios/chrome/browser/bookmarks/ui_bundled/bookmark_mediator.h"

#import <MaterialComponents/MaterialSnackbar.h>

#import "base/i18n/message_formatter.h"
#import "base/memory/raw_ptr.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_feature_list.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/bookmarks/browser/url_and_title.h"
#import "components/signin/public/base/consent_level.h"
#import "components/sync/base/user_selectable_type.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/test/test_sync_service.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_client_impl.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_ios_unit_test_support.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_storage_type.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/bookmark_utils_ios.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/browser/signin/model/fake_system_identity_manager.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/grit/ios_strings.h"
#import "testing/gtest_mac.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

NSString* const kFolderName = @"folder name";
NSString* const kEmail = @"[email protected]";

// List of cases to tests.
enum class SignInStatus {
  // The user is signed out.
  kSignOut,
  // The user is signed in and using the local or syncable storage.
  kSignedInOnlyWithLocalOrSyncableStorage,
  // The user is signed in, not syncing bookmark.
  kSignedInNoBookmarkSyncing,
  // The user is signed in and using the account storage.
  kSignedInOnlyWithAccountStorage,
  // The user is signed in and syncing.
  KSignedInAndSync
};

class BookmarkMediatorUnitTest
    : public BookmarkIOSUnitTestSupport,
      public testing::WithParamInterface<
          std::tuple<int, bool, SignInStatus, bool>> {
 public:
  void SetUp() override {
    BookmarkIOSUnitTestSupport::SetUp();
    authentication_service_ = AuthenticationServiceFactory::GetForBrowserState(
        chrome_browser_state_.get());

    mediator_ = [[BookmarkMediator alloc]
        initWithBookmarkModel:bookmark_model_.get()
                        prefs:chrome_browser_state_->GetPrefs()
        authenticationService:authentication_service_
                  syncService:&sync_service_];
  }

  // Number of bookmark saved.
  int GetBookmarkCountParam() { return std::get<0>(GetParam()); }
  // Wether the bookmarks are saved in the default folder or not.
  bool GetFolderWasSelectedByUserParam() { return std::get<1>(GetParam()); }
  SignInStatus GetSignInStatusParam() { return std::get<2>(GetParam()); }
  // Wether to display the number of bookmarks when it’s not 1.
  bool GetDisplayTheNumberOfBookmarksParam() { return std::get<3>(GetParam()); }

 protected:
  // Signs in using `fakeIdentity1`.
  FakeSystemIdentity* SignInOnly() {
    FakeSystemIdentityManager* system_identity_manager =
        FakeSystemIdentityManager::FromSystemIdentityManager(
            GetApplicationContext()->GetSystemIdentityManager());
    FakeSystemIdentity* fake_identity = [FakeSystemIdentity fakeIdentity1];
    system_identity_manager->AddIdentity(fake_identity);
    authentication_service_->SignIn(
        fake_identity,
        signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_MANAGER);
    sync_service_.SetSignedIn(signin::ConsentLevel::kSignin);
    return fake_identity;
  }

  // Signs in using `fakeIdentity1`. Disables synchronization of bookmark.
  FakeSystemIdentity* SignInOnlyAndDisableBookmark() {
    FakeSystemIdentity* fake_identity = SignInOnly();
    sync_service_.GetUserSettings()->SetSelectedType(
        syncer::UserSelectableType::kBookmarks, NO);
    return fake_identity;
  }

  // Signs in and enable sync, using the same identity than `SignInOnly()`.
  void SignInAndSync() {
    FakeSystemIdentity* fake_identity = SignInOnly();
    authentication_service_->GrantSyncConsent(
        fake_identity,
        signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_MANAGER);
    sync_service_.SetSignedIn(signin::ConsentLevel::kSync);
  }

  // Returns `IDS_IOS_BOOKMARKS_BULK_SAVED` string with `count` value.
  NSString* GetSavedToDeviceText(int count, bool show_count) {
    return base::SysUTF16ToNSString(l10n_util::GetPluralStringFUTF16(
        (show_count) ? IDS_IOS_BOOKMARKS_BULK_SAVED : IDS_IOS_BOOKMARKS_SAVED,
        count));
  }

  // Returns `IDS_IOS_BOOKMARK_PAGE_SAVED_FOLDER` string with `count` and
  // `folder_name` value.
  NSString* GetSavedToFolderText(int count,
                                 NSString* folder_name,
                                 bool show_count) {
    std::u16string pattern = l10n_util::GetStringUTF16(
        (show_count) ? IDS_IOS_BOOKMARK_PAGE_BULK_SAVED_FOLDER
                     : IDS_IOS_BOOKMARK_PAGE_SAVED_FOLDER);
    std::u16string message = base::i18n::MessageFormatter::FormatWithNamedArgs(
        pattern, "count", count, "title",
        base::SysNSStringToUTF16(folder_name));
    return base::SysUTF16ToNSString(message);
  }

  // Returns `IDS_IOS_BOOKMARK_PAGE_SAVED_INTO_ACCOUNT` string with `count` and
  // `email` value.
  NSString* GetSavedToAccountText(int count, NSString* email, bool show_count) {
    std::u16string pattern = l10n_util::GetStringUTF16(
        (show_count) ? IDS_IOS_BOOKMARK_PAGE_BULK_SAVED_INTO_ACCOUNT
                     : IDS_IOS_BOOKMARK_PAGE_SAVED_INTO_ACCOUNT);
    std::u16string message = base::i18n::MessageFormatter::FormatWithNamedArgs(
        pattern, "count", count, "email", base::SysNSStringToUTF16(email));
    return base::SysUTF16ToNSString(message);
  }

  // Returns `IDS_IOS_BOOKMARK_PAGE_SAVED_INTO_ACCOUNT` string with `count`,
  // `folder_name` and `email` value.
  NSString* GetSavedToFolderToAccountText(int count,
                                          NSString* folder_name,
                                          NSString* email,
                                          bool show_count) {
    std::u16string pattern = l10n_util::GetStringUTF16(
        (show_count) ? IDS_IOS_BOOKMARK_PAGE_BULK_SAVED_INTO_ACCOUNT_FOLDER
                     : IDS_IOS_BOOKMARK_PAGE_SAVED_INTO_ACCOUNT_FOLDER);
    std::u16string message = base::i18n::MessageFormatter::FormatWithNamedArgs(
        pattern, "count", count, "title", base::SysNSStringToUTF16(folder_name),
        "email", base::SysNSStringToUTF16(email));
    return base::SysUTF16ToNSString(message);
  }

  // Returns `IDS_IOS_BOOKMARK_PAGE_SAVED_FOLDER_TO_DEVICE` string with `count`,
  // `folder_name` and `email` value.
  NSString* GetSavedLocallyOnlyText(int count,
                                    NSString* folder_name,
                                    bool show_count) {
    std::u16string pattern = l10n_util::GetStringUTF16(
        (show_count) ? IDS_IOS_BOOKMARK_PAGE_BULK_SAVED_FOLDER_TO_DEVICE
                     : IDS_IOS_BOOKMARK_PAGE_SAVED_FOLDER_TO_DEVICE);
    std::u16string message = base::i18n::MessageFormatter::FormatWithNamedArgs(
        pattern, "count", count, "title",
        base::SysNSStringToUTF16(folder_name));
    return base::SysUTF16ToNSString(message);
  }

  BookmarkMediator* mediator_;
  raw_ptr<ChromeAccountManagerService> account_manager_service_;
  raw_ptr<AuthenticationService> authentication_service_;
  syncer::TestSyncService sync_service_;
  base::test::ScopedFeatureList scope_;
  base::HistogramTester histogram_tester_;
};

INSTANTIATE_TEST_SUITE_P(
    All,
    BookmarkMediatorUnitTest,
    testing::Combine(
        // Number of bookmarked saved.
        testing::Values(1, 2),
        // Bookmark saved in the default folder or not.
        testing::Bool(),
        // Sign-in status.
        testing::Values(SignInStatus::kSignOut,
                        SignInStatus::kSignedInOnlyWithLocalOrSyncableStorage,
                        SignInStatus::kSignedInNoBookmarkSyncing,
                        SignInStatus::kSignedInOnlyWithAccountStorage,
                        SignInStatus::KSignedInAndSync),
        // Whether or not the count should be displayed.
        testing::Bool()));

// Tests the snackbar message with all the different combinaisons with:
// * One or two saved bookmarks
// * Using the default folder or not
// * Being signed-out/signed in/signed in with account storage/signed in + sync.
TEST_P(BookmarkMediatorUnitTest, TestSnackBarMessage) {
  const int bookmark_count = GetBookmarkCountParam();
  const SignInStatus signed_in_status = GetSignInStatusParam();
  const bool folder_was_selected_by_user = GetFolderWasSelectedByUserParam();
  NSString* expected_snackbar_message = nil;
  const bookmarks::BookmarkNode* ancestor_permanent_folder =
      bookmark_model_->mobile_node();
  bool show_count = GetDisplayTheNumberOfBookmarksParam();
  switch (signed_in_status) {
    case SignInStatus::kSignedInNoBookmarkSyncing:
      SignInOnlyAndDisableBookmark();
      [[fallthrough]];
    case SignInStatus::kSignOut:
      expected_snackbar_message =
          (folder_was_selected_by_user)
              ? GetSavedToFolderText(bookmark_count, kFolderName, show_count)
              : GetSavedToDeviceText(bookmark_count, show_count);
      break;
    case SignInStatus::kSignedInOnlyWithLocalOrSyncableStorage:
      if (!folder_was_selected_by_user) {
        // If the user is signed-in, syncing bookmarks, the default folder is
        // the account folder. This case can’t occur, so there is nothing to
        // test.
        return;
      }
      expected_snackbar_message =
          GetSavedLocallyOnlyText(bookmark_count, kFolderName, show_count);
      SignInOnly();
      break;
    case SignInStatus::kSignedInOnlyWithAccountStorage:
      expected_snackbar_message =
          (folder_was_selected_by_user)
              ? GetSavedToFolderToAccountText(bookmark_count, kFolderName,
                                              kEmail, show_count)
              : GetSavedToAccountText(bookmark_count, kEmail, show_count);
      SignInOnly();
      ancestor_permanent_folder = bookmark_model_->account_mobile_node();
      break;
    case SignInStatus::KSignedInAndSync:
      expected_snackbar_message =
          (folder_was_selected_by_user)
              ? GetSavedToFolderToAccountText(bookmark_count, kFolderName,
                                              kEmail, show_count)
              : GetSavedToAccountText(bookmark_count, kEmail, show_count);
      SignInAndSync();
      // This test requires that the initial download of bookmarks completed.
      static_cast<BookmarkClientImpl*>(bookmark_model_->client())
          ->SetIsSyncFeatureEnabledIncludingBookmarksForTest();
      break;
  }

  const bookmarks::BookmarkNode* parent_folder = AddFolder(
      ancestor_permanent_folder, base::SysNSStringToUTF16(kFolderName));

  NSString* const snackbar_message =
      bookmark_utils_ios::messageForAddingBookmarksInFolder(
          parent_folder, bookmark_model_.get(), folder_was_selected_by_user,
          show_count, bookmark_count,
          AuthenticationServiceFactory::GetForBrowserState(
              chrome_browser_state_.get())
              ->GetWeakPtr(),
          &sync_service_);
  ASSERT_NSEQ(snackbar_message, expected_snackbar_message);
}

// Tests bulkAddBookmarksWithURLs with no valid URL passed.
TEST_F(BookmarkMediatorUnitTest, TestBulkSnackbarMessageNoValidURLs) {
  NSArray* URLs = @[ [[NSURL alloc] initWithString:@""] ];

  MDCSnackbarMessage* const snackbarMessage =
      [mediator_ bulkAddBookmarksWithURLs:URLs
                               viewAction:^{
                               }];

  std::vector<bookmarks::UrlAndTitle> bookmarks =
      bookmark_model_->GetUniqueUrls();

  ASSERT_EQ(0U, bookmarks.size());
  ASSERT_NSEQ(snackbarMessage.text, @"0 Bookmarks saved");
  histogram_tester_.ExpectBucketCount("IOS.Bookmarks.BulkAddURLsCount", 0, 1);
}

// Tests bulkAddBookmarksWithURLs with one valid URL passed.
TEST_F(BookmarkMediatorUnitTest, TestBulkSnackbarMessageOneValidURL) {
  NSArray* URLs = @[ [[NSURL alloc] initWithString:@"https://google.ca"] ];

  MDCSnackbarMessage* const snackbarMessage =
      [mediator_ bulkAddBookmarksWithURLs:URLs
                               viewAction:^{
                               }];

  std::vector<bookmarks::UrlAndTitle> bookmarks =
      bookmark_model_->GetUniqueUrls();

  ASSERT_EQ(1U, bookmarks.size());
  ASSERT_NSEQ(snackbarMessage.text, @"Bookmark saved");
  histogram_tester_.ExpectBucketCount("IOS.Bookmarks.BulkAddURLsCount", 1, 1);
}

// Tests bulkAddBookmarksWithURLs with two valid URLs passed.
TEST_F(BookmarkMediatorUnitTest, TestBulkSnackbarMessageTwoValidURLs) {
  NSArray* URLs = @[
    [[NSURL alloc] initWithString:@"https://google.com"],
    [[NSURL alloc] initWithString:@"https://google.fr"]
  ];

  MDCSnackbarMessage* const snackbarMessage =
      [mediator_ bulkAddBookmarksWithURLs:URLs
                               viewAction:^{
                               }];

  std::vector<bookmarks::UrlAndTitle> bookmarks =
      bookmark_model_->GetUniqueUrls();

  ASSERT_EQ(2U, bookmarks.size());
  ASSERT_NSEQ(snackbarMessage.text, @"2 Bookmarks saved");
  histogram_tester_.ExpectBucketCount("IOS.Bookmarks.BulkAddURLsCount", 2, 1);
}

// Tests bulkAddBookmarksWithURLs with a set of mixed valid and invalid URLs.
TEST_F(BookmarkMediatorUnitTest, TestBulkSnackbarMessageValidAndInvalidURLs) {
  NSArray* URLs = @[
    [[NSURL alloc] initWithString:@"https://google.com"],
    [[NSURL alloc] initWithString:@"::invalid::"],
    [[NSURL alloc] initWithString:@"https://google.fr"],
    [[NSURL alloc] initWithString:@"https://google.co.jp"]
  ];

  MDCSnackbarMessage* const snackbarMessage =
      [mediator_ bulkAddBookmarksWithURLs:URLs
                               viewAction:^{
                               }];

  std::vector<bookmarks::UrlAndTitle> bookmarks =
      bookmark_model_->GetUniqueUrls();

  ASSERT_EQ(3U, bookmarks.size());
  ASSERT_NSEQ(snackbarMessage.text, @"3 Bookmarks saved");
  histogram_tester_.ExpectBucketCount("IOS.Bookmarks.BulkAddURLsCount", 3, 1);
}

// Tests bulkAddBookmarksWithURLs with duplicate bookmarks.
TEST_F(BookmarkMediatorUnitTest, TestBulkSnackbarMessageDuplicateBookmarks) {
  NSArray* URLs = @[
    [[NSURL alloc] initWithString:@"https://google.com"],
    [[NSURL alloc] initWithString:@"::invalid::"],
    [[NSURL alloc] initWithString:@"https://google.fr"],
    [[NSURL alloc] initWithString:@"https://google.co.jp"]
  ];

  MDCSnackbarMessage* const snackbarMessage =
      [mediator_ bulkAddBookmarksWithURLs:URLs
                               viewAction:^{
                               }];

  std::vector<bookmarks::UrlAndTitle> bookmarks =
      bookmark_model_->GetUniqueUrls();

  ASSERT_EQ(3U, bookmarks.size());
  ASSERT_NSEQ(snackbarMessage.text, @"3 Bookmarks saved");
  histogram_tester_.ExpectBucketCount("IOS.Bookmarks.BulkAddURLsCount", 3, 1);

  // Try bulk adding the same URLs again, none should be added.
  MDCSnackbarMessage* const snackbarMessageDuplicates =
      [mediator_ bulkAddBookmarksWithURLs:URLs
                               viewAction:^{
                               }];

  std::vector<bookmarks::UrlAndTitle> bookmarks_dupes =
      bookmark_model_->GetUniqueUrls();

  ASSERT_EQ(3U, bookmarks_dupes.size());
  ASSERT_NSEQ(snackbarMessageDuplicates.text, @"0 Bookmarks saved");
  histogram_tester_.ExpectBucketCount("IOS.Bookmarks.BulkAddURLsCount", 3, 1);
  histogram_tester_.ExpectBucketCount("IOS.Bookmarks.BulkAddURLsCount", 0, 1);
}

// Tests bulkAddBookmarksWithURLs with no valid URL passed while signed in and
// syncing.
TEST_F(BookmarkMediatorUnitTest, TestBulkSnackbarMessageNoValidURLsSyncing) {
  SignInAndSync();
  NSArray* URLs = @[ [[NSURL alloc] initWithString:@""] ];

  MDCSnackbarMessage* const snackbarMessage =
      [mediator_ bulkAddBookmarksWithURLs:URLs
                               viewAction:^{
                               }];

  std::vector<bookmarks::UrlAndTitle> bookmarks =
      bookmark_model_->GetUniqueUrls();

  ASSERT_EQ(0U, bookmarks.size());
  ASSERT_NSEQ(snackbarMessage.text,
              @"0 Bookmarks saved in your Google Account, [email protected]");
  histogram_tester_.ExpectBucketCount("IOS.Bookmarks.BulkAddURLsCount", 0, 1);
}

// Tests bulkAddBookmarksWithURLs with one valid URL passed while signed in and
// syncing.
TEST_F(BookmarkMediatorUnitTest, TestBulkSnackbarMessageOneValidURLSyncing) {
  SignInAndSync();
  NSArray* URLs = @[ [[NSURL alloc] initWithString:@"https://google.ca"] ];

  MDCSnackbarMessage* const snackbarMessage =
      [mediator_ bulkAddBookmarksWithURLs:URLs
                               viewAction:^{
                               }];

  std::vector<bookmarks::UrlAndTitle> bookmarks =
      bookmark_model_->GetUniqueUrls();

  ASSERT_EQ(1U, bookmarks.size());
  ASSERT_NSEQ(snackbarMessage.text,
              @"Bookmark saved in your Google Account, [email protected]");
  histogram_tester_.ExpectBucketCount("IOS.Bookmarks.BulkAddURLsCount", 1, 1);
}

// Tests bulkAddBookmarksWithURLs with two valid URLs passed while signed in and
// syncing.
TEST_F(BookmarkMediatorUnitTest, TestBulkSnackbarMessageTwoValidURLsSyncing) {
  SignInAndSync();
  NSArray* URLs = @[
    [[NSURL alloc] initWithString:@"https://google.com"],
    [[NSURL alloc] initWithString:@"https://google.fr"]
  ];

  MDCSnackbarMessage* const snackbarMessage =
      [mediator_ bulkAddBookmarksWithURLs:URLs
                               viewAction:^{
                               }];

  std::vector<bookmarks::UrlAndTitle> bookmarks =
      bookmark_model_->GetUniqueUrls();

  ASSERT_EQ(2U, bookmarks.size());
  ASSERT_NSEQ(snackbarMessage.text,
              @"2 Bookmarks saved in your Google Account, [email protected]");
  histogram_tester_.ExpectBucketCount("IOS.Bookmarks.BulkAddURLsCount", 2, 1);
}

// Tests bulkAddBookmarksWithURLs with a set of mixed valid and invalid URLs
// while signed in and syncing.
TEST_F(BookmarkMediatorUnitTest,
       TestBulkSnackbarMessageValidAndInvalidURLsSyncing) {
  SignInAndSync();
  NSArray* URLs = @[
    [[NSURL alloc] initWithString:@"https://google.com"],
    [[NSURL alloc] initWithString:@"::invalid::"],
    [[NSURL alloc] initWithString:@"https://google.fr"],
    [[NSURL alloc] initWithString:@"https://google.co.jp"]
  ];

  MDCSnackbarMessage* const snackbarMessage =
      [mediator_ bulkAddBookmarksWithURLs:URLs
                               viewAction:^{
                               }];

  std::vector<bookmarks::UrlAndTitle> bookmarks =
      bookmark_model_->GetUniqueUrls();

  ASSERT_EQ(3U, bookmarks.size());
  ASSERT_NSEQ(snackbarMessage.text,
              @"3 Bookmarks saved in your Google Account, [email protected]");
  histogram_tester_.ExpectBucketCount("IOS.Bookmarks.BulkAddURLsCount", 3, 1);
}

// Tests bulkAddBookmarksWithURLs with duplicate bookmarks while signed in and
// syncing.
TEST_F(BookmarkMediatorUnitTest,
       TestBulkSnackbarMessageDuplicateBookmarksSyncing) {
  SignInAndSync();
  NSArray* URLs = @[
    [[NSURL alloc] initWithString:@"https://google.com"],
    [[NSURL alloc] initWithString:@"::invalid::"],
    [[NSURL alloc] initWithString:@"https://google.fr"],
    [[NSURL alloc] initWithString:@"https://google.co.jp"]
  ];

  MDCSnackbarMessage* const snackbarMessage =
      [mediator_ bulkAddBookmarksWithURLs:URLs
                               viewAction:^{
                               }];

  std::vector<bookmarks::UrlAndTitle> bookmarks =
      bookmark_model_->GetUniqueUrls();

  ASSERT_EQ(3U, bookmarks.size());
  ASSERT_NSEQ(snackbarMessage.text,
              @"3 Bookmarks saved in your Google Account, [email protected]");
  histogram_tester_.ExpectBucketCount("IOS.Bookmarks.BulkAddURLsCount", 3, 1);

  // Try bulk adding the same URLs again, none should be added.
  MDCSnackbarMessage* const snackbarMessageDuplicates =
      [mediator_ bulkAddBookmarksWithURLs:URLs
                               viewAction:^{
                               }];

  std::vector<bookmarks::UrlAndTitle> bookmarks_dupes =
      bookmark_model_->GetUniqueUrls();

  ASSERT_EQ(3U, bookmarks_dupes.size());
  ASSERT_NSEQ(snackbarMessageDuplicates.text,
              @"0 Bookmarks saved in your Google Account, [email protected]");
  histogram_tester_.ExpectBucketCount("IOS.Bookmarks.BulkAddURLsCount", 3, 1);
  histogram_tester_.ExpectBucketCount("IOS.Bookmarks.BulkAddURLsCount", 0, 1);
}

}  // namespace