chromium/ios/chrome/app/spotlight/bookmark_spotlight_manager_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 "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/task/sequenced_task_runner.h"
#import "base/task/single_thread_task_runner.h"
#import "base/test/ios/wait_util.h"
#import "base/test/task_environment.h"
#import "base/test/test_timeouts.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/bookmarks/browser/bookmark_node.h"
#import "components/bookmarks/test/bookmark_test_helpers.h"
#import "components/bookmarks/test/test_bookmark_client.h"
#import "components/favicon/core/large_icon_service_impl.h"
#import "components/favicon/core/test/mock_favicon_service.h"
#import "ios/chrome/app/spotlight/bookmarks_spotlight_manager.h"
#import "ios/chrome/app/spotlight/fake_searchable_item_factory.h"
#import "ios/chrome/app/spotlight/fake_spotlight_interface.h"
#import "ios/chrome/app/spotlight/spotlight_manager.h"
#import "ios/chrome/app/spotlight/spotlight_util.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_ios_unit_test_support.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "net/base/apple/url_conversions.h"
#import "testing/gmock/include/gmock/gmock.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
#import "third_party/skia/include/core/SkBitmap.h"
#import "ui/base/test/ios/ui_image_test_utils.h"

using testing::_;
using ui::test::uiimage_utils::UIImageWithSizeAndSolidColor;

namespace {
const char kDummyIconUrl[] = "http://www.example.com/touch_icon.png";

favicon_base::FaviconRawBitmapResult CreateTestBitmap(int w, int h) {
  favicon_base::FaviconRawBitmapResult result;
  result.expired = false;

  CGSize size = CGSizeMake(w, h);
  UIImage* favicon = UIImageWithSizeAndSolidColor(size, [UIColor redColor]);
  NSData* png = UIImagePNGRepresentation(favicon);
  scoped_refptr<base::RefCountedBytes> data(new base::RefCountedBytes(
      static_cast<const unsigned char*>([png bytes]), [png length]));

  result.bitmap_data = data;
  result.pixel_size = gfx::Size(w, h);
  result.icon_url = GURL(kDummyIconUrl);
  result.icon_type = favicon_base::IconType::kTouchIcon;
  CHECK(result.is_valid());
  return result;
}

}  // namespace

class BookmarkSpotlightManagerTest : public BookmarkIOSUnitTestSupport {
 public:
  BookmarkSpotlightManagerTest() {
    CreateMockLargeIconService();
    spotlightInterface_ = [[FakeSpotlightInterface alloc] init];
    searchableItemFactory_ = [[FakeSearchableItemFactory alloc]
        initWithDomain:spotlight::DOMAIN_BOOKMARKS];
  }

 protected:
  void CreateMockLargeIconService() {
    large_icon_service_.reset(new favicon::LargeIconServiceImpl(
        &mock_favicon_service_, /*image_fetcher=*/nullptr,
        /*desired_size_in_dip_for_server_requests=*/0,
        /*icon_type_for_server_requests=*/favicon_base::IconType::kTouchIcon,
        /*google_server_client_param=*/"test_chrome"));

    EXPECT_CALL(mock_favicon_service_,
                GetLargestRawFaviconForPageURL(_, _, _, _, _))
        .WillRepeatedly([](auto, auto, auto,
                           favicon_base::FaviconRawBitmapCallback callback,
                           base::CancelableTaskTracker* tracker) {
          return tracker->PostTask(
              base::SingleThreadTaskRunner::GetCurrentDefault().get(),
              FROM_HERE,
              base::BindOnce(std::move(callback), CreateTestBitmap(24, 24)));
        });
  }

  testing::StrictMock<favicon::MockFaviconService> mock_favicon_service_;
  std::unique_ptr<favicon::LargeIconServiceImpl> large_icon_service_;
  base::CancelableTaskTracker cancelable_task_tracker_;
  FakeSpotlightInterface* spotlightInterface_;
  FakeSearchableItemFactory* searchableItemFactory_;
};

/// Tests that clearAndReindexModel actually clears all bookmarks items and
/// attempt to reindex the existing items in bookmark.
TEST_F(BookmarkSpotlightManagerTest, testClearAndReindexModel) {
  AddBookmark(bookmark_model_->mobile_node(), u"foo1", GURL("http://foo1.com"));
  AddBookmark(bookmark_model_->account_mobile_node(), u"foo2",
              GURL("http://foo2.com"));

  FakeSpotlightInterface* fakeSpotlightInterface =
      [[FakeSpotlightInterface alloc] init];

  BookmarksSpotlightManager* manager = [[BookmarksSpotlightManager alloc]
      initWithLargeIconService:large_icon_service_.get()
                 bookmarkModel:bookmark_model_
            spotlightInterface:fakeSpotlightInterface
         searchableItemFactory:searchableItemFactory_
                   prefService:pref_service_];

  NSUInteger initialIndexedItemCount =
      fakeSpotlightInterface.indexSearchableItemsCallsCount;

  [manager clearAndReindexModel];

  // We expect to attempt deleting searchable items.
  ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      TestTimeouts::action_timeout(), ^bool() {
        return fakeSpotlightInterface
                   .deleteSearchableItemsWithDomainIdentifiersCallsCount == 1u;
      }));

  // We expect that we will reindex the only existing item in bookmark,
  // thus the +2 for the count.
  ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      TestTimeouts::action_timeout(), ^bool() {
        return fakeSpotlightInterface.indexSearchableItemsCallsCount ==
               initialIndexedItemCount + 2;
      }));

  [manager shutdown];
}

/// Tests that when calling parentFolderNamesForNode giving a bookmark node, it
/// returns an array of its ancestor folder names
TEST_F(BookmarkSpotlightManagerTest, testParentFolderNamesForNode) {
  BookmarksSpotlightManager* manager = [[BookmarksSpotlightManager alloc]
      initWithLargeIconService:large_icon_service_.get()
                 bookmarkModel:bookmark_model_
            spotlightInterface:spotlightInterface_
         searchableItemFactory:searchableItemFactory_
                   prefService:pref_service_];

  const bookmarks::BookmarkNode* root = bookmark_model_->mobile_node();
  static const std::string model_string("a 1:[ b c ] d 2:[ 21:[ e ] f g ] h ");
  bookmarks::test::AddNodesFromModelString(bookmark_model_, root, model_string);
  const bookmarks::BookmarkNode* eNode =
      root->children()[3]->children().front()->children().front().get();
  NSMutableArray* folderNames = [manager parentFolderNamesForNode:eNode];

  EXPECT_EQ([folderNames count], 2u);
  EXPECT_TRUE([[folderNames objectAtIndex:0] isEqualToString:@"2"]);
  EXPECT_TRUE([[folderNames objectAtIndex:1] isEqualToString:@"21"]);

  [manager shutdown];
}

/// Tests that when we add a new bookmark item, we actually try to
/// index/refresh it in spotlight.
TEST_F(BookmarkSpotlightManagerTest, testRefreshItemWithURL) {
  FakeSpotlightInterface* fakeSpotlightInterface =
      [[FakeSpotlightInterface alloc] init];

  BookmarksSpotlightManager* manager = [[BookmarksSpotlightManager alloc]
      initWithLargeIconService:large_icon_service_.get()
                 bookmarkModel:bookmark_model_
            spotlightInterface:fakeSpotlightInterface
         searchableItemFactory:searchableItemFactory_
                   prefService:pref_service_];

  NSUInteger initialIndexedItemCount =
      fakeSpotlightInterface.indexSearchableItemsCallsCount;

  AddBookmark(bookmark_model_->mobile_node(), u"foo1", GURL("http://foo1.com"));
  AddBookmark(bookmark_model_->account_mobile_node(), u"foo2",
              GURL("http://foo2.com"));

  // We expect to call indexSearchableItems api method to add the new added
  // bookmark items.
  EXPECT_EQ(fakeSpotlightInterface.indexSearchableItemsCallsCount,
            initialIndexedItemCount + 2);

  [manager shutdown];
}

/// Tests that when we update a bookmark item, we actually try to remove it from
/// spotlight and reindex it.
TEST_F(BookmarkSpotlightManagerTest, testUpdateBookmarkItem) {
  FakeSpotlightInterface* fakeSpotlightInterface =
      [[FakeSpotlightInterface alloc] init];

  BookmarksSpotlightManager* manager = [[BookmarksSpotlightManager alloc]
      initWithLargeIconService:large_icon_service_.get()
                 bookmarkModel:bookmark_model_
            spotlightInterface:fakeSpotlightInterface
         searchableItemFactory:searchableItemFactory_
                   prefService:pref_service_];

  NSUInteger currentIndexedItemCount =
      fakeSpotlightInterface.indexSearchableItemsCallsCount;

  const bookmarks::BookmarkNode* addedNode1 = AddBookmark(
      bookmark_model_->mobile_node(), u"foo1", GURL("http://foo1.com"));
  const bookmarks::BookmarkNode* addedNode2 = AddBookmark(
      bookmark_model_->account_mobile_node(), u"foo2", GURL("http://foo2.com"));

  // We expect to call indexSearchableItems api method to add the new added
  // bookmark item.
  EXPECT_EQ(fakeSpotlightInterface.indexSearchableItemsCallsCount,
            currentIndexedItemCount + 2);

  currentIndexedItemCount =
      fakeSpotlightInterface.indexSearchableItemsCallsCount;

  bookmark_model_->SetTitle(addedNode1, u"new title 1",
                            bookmarks::metrics::BookmarkEditSource::kOther);
  bookmark_model_->SetTitle(addedNode2, u"new title 2",
                            bookmarks::metrics::BookmarkEditSource::kOther);

  // We expect to delete the modified item using its identifier.
  EXPECT_EQ(
      fakeSpotlightInterface.deleteSearchableItemsWithIdentifiersCallsCount,
      2u);

  // We expect reindexing it with the new details.
  EXPECT_EQ(fakeSpotlightInterface.indexSearchableItemsCallsCount,
            currentIndexedItemCount + 2);

  [manager shutdown];
}

/// Tests that clearAndReindexModel only clears out items if the bookmark models
/// are undefined.
TEST_F(BookmarkSpotlightManagerTest, testIndexAllBookmarksWithNoBookmarkModel) {
  FakeSpotlightInterface* fakeSpotlightInterface =
      [[FakeSpotlightInterface alloc] init];

  // Intialize the BookmarksSpotlightManager with a state where bookmarkModels
  // are undefined.
  BookmarksSpotlightManager* manager = [[BookmarksSpotlightManager alloc]
      initWithLargeIconService:large_icon_service_.get()
                 bookmarkModel:nullptr
            spotlightInterface:fakeSpotlightInterface
         searchableItemFactory:searchableItemFactory_
                   prefService:pref_service_];

  NSUInteger initialIndexedItemCount =
      fakeSpotlightInterface.indexSearchableItemsCallsCount;

  [manager clearAndReindexModel];

  // We expect to attempt deleting searchable items.
  ASSERT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      TestTimeouts::action_timeout(), ^bool() {
        return fakeSpotlightInterface
                   .deleteSearchableItemsWithDomainIdentifiersCallsCount == 1u;
      }));

  // We expect that we will never attempt to index any items since bookmark
  // model are undefined.
  EXPECT_EQ(fakeSpotlightInterface.indexSearchableItemsCallsCount,
            initialIndexedItemCount);

  [manager shutdown];
}

/// Tests that when the bookmark model updates while the app is in background,
/// the update doesn't immediately happen in the index; and at foregrounding,
/// the manager reindexes everything.
TEST_F(BookmarkSpotlightManagerTest, testUpdatesInBackgroundCauseFullReindex) {
  FakeSpotlightInterface* fakeSpotlightInterface =
      [[FakeSpotlightInterface alloc] init];

  BookmarksSpotlightManager* manager = [[BookmarksSpotlightManager alloc]
      initWithLargeIconService:large_icon_service_.get()
                 bookmarkModel:bookmark_model_
            spotlightInterface:fakeSpotlightInterface
         searchableItemFactory:searchableItemFactory_
                   prefService:pref_service_];

  const bookmarks::BookmarkNode* addedNode1 = AddBookmark(
      bookmark_model_->mobile_node(), u"foo1", GURL("http://foo1.com"));
  AddBookmark(bookmark_model_->account_mobile_node(), u"foo2",
              GURL("http://foo2.com"));

  EXPECT_EQ(fakeSpotlightInterface.indexSearchableItemsCallsCount, 2u);

  [[NSNotificationCenter defaultCenter]
      postNotificationName:UIApplicationDidEnterBackgroundNotification
                    object:nil
                  userInfo:nil];

  bookmark_model_->SetTitle(addedNode1, u"new title 1",
                            bookmarks::metrics::BookmarkEditSource::kOther);

  // Update shouldn't happen until we reach foreground.
  EXPECT_EQ(fakeSpotlightInterface.indexSearchableItemsCallsCount, 2u);

  [[NSNotificationCenter defaultCenter]
      postNotificationName:UIApplicationWillEnterForegroundNotification
                    object:nil
                  userInfo:nil];

  // We expect not to delete the modified item using its identifier.
  EXPECT_EQ(
      fakeSpotlightInterface.deleteSearchableItemsWithIdentifiersCallsCount,
      0u);

  // We expect all 2 items to be removed by domain identifier instead.
  EXPECT_EQ(fakeSpotlightInterface
                .deleteSearchableItemsWithDomainIdentifiersCallsCount,
            1u);

  // We expect reindexing both items.
  EXPECT_EQ(fakeSpotlightInterface.indexSearchableItemsCallsCount, 4u);

  [manager shutdown];
}