chromium/ios/chrome/app/spotlight/reading_list_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 "ios/chrome/app/spotlight/reading_list_spotlight_manager.h"

#import "base/location.h"
#import "base/memory/raw_ptr.h"
#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 "components/favicon/core/large_icon_service_impl.h"
#import "components/favicon/core/test/mock_favicon_service.h"
#import "components/reading_list/core/reading_list_model.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/reading_list/model/reading_list_model_factory.h"
#import "ios/chrome/browser/reading_list/model/reading_list_test_utils.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 "testing/platform_test.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 kTestURL1[] = "http://www.example1.com/";
const char kTestURL2[] = "http://www.example2.com/";
const char kTestURL3[] = "http://www.example3.com/";

const char kDummyIconUrl[] = "http://www.example.com/touch_icon.png";
const char kTestTitle1[] = "Test Reading List Item Title1";
const char kTestTitle2[] = "Test Reading List Item Title2";
const char kTestTitle3[] = "Test Reading List Item Title3";

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 ReadingListSpotlightManagerTest : public PlatformTest {
 public:
  ReadingListSpotlightManagerTest() {
    std::vector<scoped_refptr<ReadingListEntry>> initial_entries;
    initial_entries.push_back(base::MakeRefCounted<ReadingListEntry>(
        GURL(kTestURL1), kTestTitle1, base::Time::Now()));
    initial_entries.push_back(base::MakeRefCounted<ReadingListEntry>(
        GURL(kTestURL2), kTestTitle2, base::Time::Now()));

    TestChromeBrowserState::Builder builder;
    builder.AddTestingFactory(
        ReadingListModelFactory::GetInstance(),
        base::BindRepeating(&BuildReadingListModelWithFakeStorage,
                            std::move(initial_entries)));

    browser_state_ = std::move(builder).Build();

    model_ = ReadingListModelFactory::GetInstance()->GetForBrowserState(
        browser_state_.get());

    CreateMockLargeIconService();
    spotlightInterface_ = [[FakeSpotlightInterface alloc] init];

    searchableItemFactory_ = [[FakeSearchableItemFactory alloc]
        initWithDomain:spotlight::DOMAIN_READING_LIST];
  }

 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)));
        });
  }

  base::test::TaskEnvironment task_environment_;
  std::unique_ptr<TestChromeBrowserState> browser_state_;
  testing::StrictMock<favicon::MockFaviconService> mock_favicon_service_;
  std::unique_ptr<favicon::LargeIconServiceImpl> large_icon_service_;
  base::CancelableTaskTracker cancelable_task_tracker_;
  raw_ptr<ReadingListModel> model_;
  FakeSpotlightInterface* spotlightInterface_;
  FakeSearchableItemFactory* searchableItemFactory_;
};

/// Tests that init propagates the `model` and -shutdown removes it.
TEST_F(ReadingListSpotlightManagerTest, testInitAndShutdown) {
  ReadingListSpotlightManager* manager = [[ReadingListSpotlightManager alloc]
      initWithLargeIconService:large_icon_service_.get()
              readingListModel:model_
            spotlightInterface:spotlightInterface_
         searchableItemFactory:searchableItemFactory_];

  EXPECT_EQ(manager.model, model_);
  [manager shutdown];
  EXPECT_EQ(manager.model, nil);
}

/// Tests that clearAndReindexReadingList actually clears all items (by calling
/// spotlight api) and adds the items ( by calling the class method
/// indexAllReadingListItemsg)
TEST_F(ReadingListSpotlightManagerTest, testClearsAndIndexesItems) {
  FakeSpotlightInterface* fakeSpotlightInterface =
      [[FakeSpotlightInterface alloc] init];

  // When the model is loaded we call clearAndReindexReadingList
  ReadingListSpotlightManager* manager = [[ReadingListSpotlightManager alloc]
      initWithLargeIconService:large_icon_service_.get()
              readingListModel:model_
            spotlightInterface:fakeSpotlightInterface
         searchableItemFactory:searchableItemFactory_];

  // We expect to attempt deleting searchable items.
  EXPECT_EQ(fakeSpotlightInterface
                .deleteSearchableItemsWithDomainIdentifiersCallsCount,
            1u);

  // We expect that we call indexSearchableItems api twice because the fake
  // reading list storage initially contains 2 items.
  EXPECT_EQ(fakeSpotlightInterface.indexSearchableItemsCallsCount, 2u);

  [manager shutdown];
}

/// Test that adding an entry via the app (ADDED_VIA_CURRENT_APP) actually adds
/// the entry to spotlight via the indexSearchableItemApi
TEST_F(ReadingListSpotlightManagerTest, testAddEntry) {
  FakeSpotlightInterface* fakeSpotlightInterface =
      [[FakeSpotlightInterface alloc] init];

  // When the model is loaded we call clearAndReindexReadingList
  ReadingListSpotlightManager* manager = [[ReadingListSpotlightManager alloc]
      initWithLargeIconService:large_icon_service_.get()
              readingListModel:model_
            spotlightInterface:fakeSpotlightInterface
         searchableItemFactory:searchableItemFactory_];

  NSUInteger initialIndexedItemCount =
      fakeSpotlightInterface.indexSearchableItemsCallsCount;

  model_->AddOrReplaceEntry(GURL(kTestURL3), kTestTitle3,
                            reading_list::ADDED_VIA_CURRENT_APP,
                            /*estimated_read_time=*/base::TimeDelta());

  // We expect that we call indexSearchableItems spotlight api when adding a new
  // entry in reading list.
  EXPECT_EQ(fakeSpotlightInterface.indexSearchableItemsCallsCount,
            initialIndexedItemCount + 1);

  [manager shutdown];
}

/// Test that removing an entry  actually
/// removes the entry from spotlight via calling
/// deleteSearchableItemsWithIdentifiers spotlight api.
TEST_F(ReadingListSpotlightManagerTest, testRemoveEntry) {
  FakeSpotlightInterface* fakeSpotlightInterface =
      [[FakeSpotlightInterface alloc] init];

  ReadingListSpotlightManager* manager = [[ReadingListSpotlightManager alloc]
      initWithLargeIconService:large_icon_service_.get()
              readingListModel:model_
            spotlightInterface:fakeSpotlightInterface
         searchableItemFactory:searchableItemFactory_];

  model_->RemoveEntryByURL(GURL(kTestURL1), FROM_HERE);

  // We expect to attempt deleting the item that was removed, from spotlight.
  EXPECT_EQ(
      fakeSpotlightInterface.deleteSearchableItemsWithIdentifiersCallsCount,
      1u);

  [manager shutdown];
}

// Test that model updates in background don't do anything until the app is
// foregrounded, at which point they cause a full reindex.
TEST_F(ReadingListSpotlightManagerTest, testBackgroundPausesModelUpdates) {
  FakeSpotlightInterface* fakeSpotlightInterface =
      [[FakeSpotlightInterface alloc] init];

  ReadingListSpotlightManager* manager = [[ReadingListSpotlightManager alloc]
      initWithLargeIconService:large_icon_service_.get()
              readingListModel:model_
            spotlightInterface:fakeSpotlightInterface
         searchableItemFactory:searchableItemFactory_];

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

  EXPECT_EQ(fakeSpotlightInterface
                .deleteSearchableItemsWithDomainIdentifiersCallsCount,
            1u);

  model_->RemoveEntryByURL(GURL(kTestURL1), FROM_HERE);

  EXPECT_EQ(
      fakeSpotlightInterface.deleteSearchableItemsWithIdentifiersCallsCount,
      0u);
  EXPECT_EQ(fakeSpotlightInterface
                .deleteSearchableItemsWithDomainIdentifiersCallsCount,
            1u);

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

  EXPECT_EQ(
      fakeSpotlightInterface.deleteSearchableItemsWithIdentifiersCallsCount,
      0u);
  EXPECT_EQ(fakeSpotlightInterface
                .deleteSearchableItemsWithDomainIdentifiersCallsCount,
            2u);

  [manager shutdown];
}

// Test that attempting public API calls don't have an immediate effect in
// background, and the update only happens when the app is foregrounded again.
TEST_F(ReadingListSpotlightManagerTest, testBackgroundPausesAPICalls) {
  FakeSpotlightInterface* fakeSpotlightInterface =
      [[FakeSpotlightInterface alloc] init];

  ReadingListSpotlightManager* manager = [[ReadingListSpotlightManager alloc]
      initWithLargeIconService:large_icon_service_.get()
              readingListModel:model_
            spotlightInterface:fakeSpotlightInterface
         searchableItemFactory:searchableItemFactory_];
  EXPECT_EQ(fakeSpotlightInterface
                .deleteSearchableItemsWithDomainIdentifiersCallsCount,
            1u);

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

  [manager clearAndReindexReadingList];
  EXPECT_EQ(fakeSpotlightInterface
                .deleteSearchableItemsWithDomainIdentifiersCallsCount,
            1u);

  [[NSNotificationCenter defaultCenter]
      postNotificationName:UIApplicationWillEnterForegroundNotification
                    object:nil
                  userInfo:nil];
  EXPECT_EQ(fakeSpotlightInterface
                .deleteSearchableItemsWithDomainIdentifiersCallsCount,
            2u);

  [manager shutdown];
}