chromium/chrome/browser/reading_list/android/reading_list_manager_impl_unittest.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/reading_list/android/reading_list_manager_impl.h"

#include <cstdint>
#include <memory>
#include <string>
#include <utility>

#include "base/location.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/simple_test_clock.h"
#include "base/uuid.h"
#include "chrome/browser/reading_list/android/reading_list_manager.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/browser/bookmark_utils.h"
#include "components/reading_list/core/fake_reading_list_model_storage.h"
#include "components/reading_list/core/reading_list_model_impl.h"
#include "components/sync/base/storage_type.h"
#include "components/sync/model/wipe_model_upon_sync_disabled_behavior.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using BookmarkNode = bookmarks::BookmarkNode;

namespace {

constexpr char kURL[] = "https://www.example.com";
constexpr char kURL1[] = "https://www.anotherexample.com";
constexpr char kTitle[] =
    "In earlier tellings, the dog had a better reputation than the cat, "
    "however the president vetoed it.";
constexpr char kTitle1[] = "boring title about dogs.";
constexpr char kReadStatusKey[] = "read_status";
constexpr char kReadStatusRead[] = "true";
constexpr char kReadStatusUnread[] = "false";
constexpr char kInvalidUTF8[] = "\xc3\x28";

class MockObserver : public ReadingListManager::Observer {
 public:
  MockObserver() = default;
  ~MockObserver() override = default;

  // ReadingListManager::Observer implementation.
  MOCK_METHOD(void, ReadingListLoaded, (), (override));
  MOCK_METHOD(void, ReadingListChanged, (), (override));
};

class ReadingListManagerImplTest : public testing::Test {
 public:
  ReadingListManagerImplTest() = default;
  ~ReadingListManagerImplTest() override = default;

  void SetUp() override {
    clock_.SetNow(base::Time::Now());
    EXPECT_TRUE(ResetStorage()->TriggerLoadCompletion());
    EXPECT_TRUE(manager()->IsLoaded());
  }

  base::WeakPtr<FakeReadingListModelStorage> ResetStorage() {
    manager_.reset();
    reading_list_model_.reset();

    auto storage = std::make_unique<FakeReadingListModelStorage>();
    base::WeakPtr<FakeReadingListModelStorage> storage_ptr =
        storage->AsWeakPtr();

    reading_list_model_ = std::make_unique<ReadingListModelImpl>(
        std::move(storage), syncer::StorageType::kUnspecified,
        syncer::WipeModelUponSyncDisabledBehavior::kNever, &clock_);
    manager_ = std::make_unique<ReadingListManagerImpl>(
        reading_list_model_.get(),
        base::BindRepeating([](int64_t* id) { return (*id)++; },
                            base::Owned(std::make_unique<int64_t>(0))));
    manager_->AddObserver(observer());

    return storage_ptr;
  }

  void TearDown() override { manager_->RemoveObserver(observer()); }

 protected:
  ReadingListManager* manager() { return manager_.get(); }
  ReadingListModelImpl* reading_list_model() {
    return reading_list_model_.get();
  }
  base::SimpleTestClock* clock() { return &clock_; }
  MockObserver* observer() { return &observer_; }

  const BookmarkNode* Add(const GURL& url, const std::string& title) {
    EXPECT_CALL(*observer(), ReadingListChanged());
    return manager()->Add(url, title);
  }

  void Delete(const GURL& url) {
    EXPECT_CALL(*observer(), ReadingListChanged());
    manager()->Delete(url);
  }

  void SetReadStatus(const GURL& url, bool read) {
    EXPECT_CALL(*observer(), ReadingListChanged());
    manager()->SetReadStatus(url, read);
  }

 private:
  base::SimpleTestClock clock_;
  std::unique_ptr<ReadingListModelImpl> reading_list_model_;
  std::unique_ptr<ReadingListManager> manager_;
  MockObserver observer_;
};

// Verifies the states without any reading list data.
TEST_F(ReadingListManagerImplTest, RootWithEmptyReadingList) {
  const auto* root = manager()->GetRoot();
  ASSERT_TRUE(root);
  EXPECT_TRUE(root->is_folder());
  EXPECT_EQ(0u, manager()->size());
}

// Verifies load data into reading list model will update |manager_| as well.
TEST_F(ReadingListManagerImplTest, Load) {
  base::WeakPtr<FakeReadingListModelStorage> fake_storage = ResetStorage();
  ASSERT_FALSE(manager()->IsLoaded());

  // Mimic the completion of storage loading with one initial entry.
  std::vector<scoped_refptr<ReadingListEntry>> entries;
  GURL url(kURL);
  entries.push_back(
      base::MakeRefCounted<ReadingListEntry>(url, kTitle, clock()->Now()));
  ASSERT_TRUE(fake_storage->TriggerLoadCompletion(std::move(entries)));
  EXPECT_TRUE(manager()->IsLoaded());

  const auto* node = manager()->Get(url);
  EXPECT_TRUE(node);
  EXPECT_EQ(url, node->url());
  EXPECT_EQ(clock()->Now(), node->date_added());
  EXPECT_EQ(1u, manager()->unread_size());
}

// Verifes Add(), Get(), Delete() API in reading list manager.
TEST_F(ReadingListManagerImplTest, AddGetDelete) {
  // Adds a node.
  GURL url(kURL);
  Add(url, kTitle);
  EXPECT_EQ(1u, manager()->size());
  EXPECT_EQ(1u, manager()->unread_size());
  EXPECT_EQ(1u, manager()->GetRoot()->children().size())
      << "The reading list node should be the child of the root.";

  // Gets the node, and verifies its content.
  const BookmarkNode* node = manager()->Get(url);
  ASSERT_TRUE(node);
  EXPECT_EQ(url, node->url());
  EXPECT_EQ(kTitle, base::UTF16ToUTF8(node->GetTitle()));
  std::string read_status;
  node->GetMetaInfo(kReadStatusKey, &read_status);
  EXPECT_EQ(kReadStatusUnread, read_status)
      << "By default the reading list node is marked as unread.";

  // Gets an invalid URL.
  EXPECT_EQ(nullptr, manager()->Get(GURL("invalid spec")));

  // Deletes the node.
  Delete(url);
  EXPECT_EQ(0u, manager()->size());
  EXPECT_EQ(0u, manager()->unread_size());
  EXPECT_TRUE(manager()->GetRoot()->children().empty());
}

TEST_F(ReadingListManagerImplTest, DeleteAllEntries) {
  // Adds a node.
  GURL url(kURL);
  Add(url, kTitle);
  EXPECT_EQ(1u, manager()->size());
  EXPECT_EQ(1u, manager()->unread_size());
  EXPECT_EQ(1u, manager()->GetRoot()->children().size())
      << "The reading list node should be the child of the root.";

  EXPECT_CALL(*observer(), ReadingListChanged()).RetiresOnSaturation();
  // Deletes the node.
  manager()->DeleteAll();
  EXPECT_EQ(0u, manager()->size());
  EXPECT_EQ(0u, manager()->unread_size());
  EXPECT_TRUE(manager()->GetRoot()->children().empty());
}

// Verifies GetNodeByID() and IsReadingListBookmark() works correctly.
TEST_F(ReadingListManagerImplTest, GetNodeByIDIsReadingListBookmark) {
  GURL url(kURL);
  const auto* node = Add(url, kTitle);

  // Find the root.
  EXPECT_EQ(manager()->GetRoot(),
            manager()->GetNodeByID(manager()->GetRoot()->id()));
  EXPECT_TRUE(manager()->IsReadingListBookmark(manager()->GetRoot()));

  // Find existing node.
  EXPECT_EQ(node, manager()->GetNodeByID(node->id()));
  EXPECT_TRUE(manager()->IsReadingListBookmark(node));

  // Non existing node.
  node = manager()->GetNodeByID(12345);
  EXPECT_FALSE(node);
  EXPECT_FALSE(manager()->IsReadingListBookmark(node));

  // Node with the same URL but not in the tree.
  auto node_same_url =
      std::make_unique<BookmarkNode>(0, base::Uuid::GenerateRandomV4(), url);
  EXPECT_FALSE(manager()->IsReadingListBookmark(node_same_url.get()));
}

// Verifies GetMatchingNodes() API in reading list manager.
TEST_F(ReadingListManagerImplTest, GetMatchingNodes) {
  manager()->Add(GURL(kURL), kTitle);
  manager()->Add(GURL(kURL1), kTitle1);
  EXPECT_EQ(2u, manager()->size());

  // Search with a multi-word query text.
  std::vector<const BookmarkNode*> results;
  bookmarks::QueryFields query;
  query.word_phrase_query = std::make_unique<std::u16string>(u"dog cat");
  manager()->GetMatchingNodes(query, 5, &results);
  EXPECT_EQ(1u, results.size());

  // Search with a single word query text.
  results.clear();
  query.word_phrase_query = std::make_unique<std::u16string>(u"dog");
  manager()->GetMatchingNodes(query, 5, &results);
  EXPECT_EQ(2u, results.size());

  // Search with empty string. Shouldn't match anything.
  results.clear();
  query.word_phrase_query = std::make_unique<std::u16string>();
  manager()->GetMatchingNodes(query, 5, &results);
  EXPECT_EQ(0u, results.size());
}

TEST_F(ReadingListManagerImplTest, GetMatchingNodesWithMaxCount) {
  manager()->Add(GURL(kURL), kTitle);
  manager()->Add(GURL(kURL1), kTitle1);
  EXPECT_EQ(2u, manager()->size());

  // Search with a query text.
  std::vector<const BookmarkNode*> results;
  bookmarks::QueryFields query;
  query.word_phrase_query = std::make_unique<std::u16string>(u"dog");
  manager()->GetMatchingNodes(query, 5, &results);
  EXPECT_EQ(2u, results.size());

  // Search with having pre-existing elements in |results|.
  manager()->GetMatchingNodes(query, 5, &results);
  EXPECT_EQ(4u, results.size());

  // Max count should never be exceeded.
  manager()->GetMatchingNodes(query, 5, &results);
  EXPECT_EQ(5u, results.size());
  manager()->GetMatchingNodes(query, 5, &results);
  EXPECT_EQ(5u, results.size());
}

// If Add() the same URL twice, the first bookmark node pointer will be
// invalidated.
TEST_F(ReadingListManagerImplTest, AddTwice) {
  // Adds a node twice.
  GURL url(kURL);
  Add(url, kTitle);
  const auto* new_node = Add(url, kTitle1);
  EXPECT_EQ(kTitle1, base::UTF16ToUTF8(new_node->GetTitle()));
  EXPECT_EQ(url, new_node->url());
}

// If Add() with an invalid title, nullptr will be returned.
TEST_F(ReadingListManagerImplTest, AddInvalidTitle) {
  GURL url(kURL);

  // Use an invalid UTF8 string.
  std::u16string dummy;
  EXPECT_FALSE(
      base::UTF8ToUTF16(kInvalidUTF8, std::size(kInvalidUTF8), &dummy));
  const auto* new_node = Add(url, std::string(kInvalidUTF8));
  EXPECT_EQ(nullptr, new_node)
      << "Should return nullptr when failed to parse the title.";
}

// If Add() with an invalid URL, nullptr will be returned.
TEST_F(ReadingListManagerImplTest, AddInvalidURL) {
  GURL invalid_url("chrome://flags");
  EXPECT_FALSE(reading_list_model()->IsUrlSupported(invalid_url));

  // Use an invalid URL, the observer method ReadingListDidAddEntry() won't be
  // invoked.
  const auto* new_node = manager()->Add(invalid_url, kTitle);
  EXPECT_EQ(nullptr, new_node)
      << "Should return nullptr when the URL scheme is not supported.";
}

// Verifes SetReadStatus()/GetReadStatus() API.
TEST_F(ReadingListManagerImplTest, ReadStatus) {
  GURL url(kURL);

  // No op when no reading list entries.
  manager()->SetReadStatus(url, true);
  EXPECT_EQ(0u, manager()->size());

  // Add a node and mark as read.
  Add(url, kTitle);
  SetReadStatus(url, true);

  const BookmarkNode* node = manager()->Get(url);
  ASSERT_TRUE(node);
  EXPECT_EQ(url, node->url());
  std::string read_status;
  node->GetMetaInfo(kReadStatusKey, &read_status);
  EXPECT_EQ(kReadStatusRead, read_status);
  EXPECT_EQ(0u, manager()->unread_size());
  EXPECT_TRUE(manager()->GetReadStatus(node));

  // Mark as unread.
  SetReadStatus(url, false);
  node = manager()->Get(url);
  node->GetMetaInfo(kReadStatusKey, &read_status);
  EXPECT_EQ(kReadStatusUnread, read_status);
  EXPECT_EQ(1u, manager()->unread_size());
  EXPECT_FALSE(manager()->GetReadStatus(node));

  // Node not in the reading list should return false.
  auto other_node =
      std::make_unique<BookmarkNode>(0, base::Uuid::GenerateRandomV4(), url);
  EXPECT_FALSE(manager()->GetReadStatus(node));

  // Root node should return false.
  EXPECT_FALSE(manager()->GetReadStatus(manager()->GetRoot()));
}

// Verifies the bookmark node is added when sync or other source adds the
// reading list entry from |reading_list_model_|.
TEST_F(ReadingListManagerImplTest, ReadingListDidAddEntry) {
  GURL url(kURL);
  EXPECT_CALL(*observer(), ReadingListChanged()).RetiresOnSaturation();
  reading_list_model()->AddOrReplaceEntry(
      url, kTitle, reading_list::ADDED_VIA_SYNC,
      /*estimated_read_time=*/base::TimeDelta());

  const auto* node = manager()->Get(url);
  EXPECT_TRUE(node);
  EXPECT_EQ(url, node->url());
  EXPECT_EQ(1u, manager()->size());
}

// Verifies the bookmark node is deleted when sync or other source deletes the
// reading list entry from |reading_list_model_|.
TEST_F(ReadingListManagerImplTest, ReadingListWillRemoveEntry) {
  GURL url(kURL);

  // Adds a node.
  const auto* node = Add(url, kTitle);
  EXPECT_TRUE(node);
  EXPECT_EQ(url, node->url());
  EXPECT_EQ(1u, manager()->size());

  // Removes it from |reading_list_model_|.
  EXPECT_CALL(*observer(), ReadingListChanged()).RetiresOnSaturation();
  reading_list_model()->RemoveEntryByURL(url, FROM_HERE);
  node = manager()->Get(url);
  EXPECT_FALSE(node);
  EXPECT_EQ(0u, manager()->size());
}

// Verifies the bookmark node is updated when sync or other source updates the
// reading list entry from |reading_list_model_|.
TEST_F(ReadingListManagerImplTest, ReadingListWillMoveEntry) {
  GURL url(kURL);

  // Adds a node.
  const auto* node = Add(url, kTitle);
  EXPECT_TRUE(node);
  EXPECT_FALSE(manager()->GetReadStatus(node));

  SetReadStatus(url, true);
  EXPECT_TRUE(manager()->GetReadStatus(node));
}

TEST_F(ReadingListManagerImplTest, EmptyBatchUpdatesDontTriggerObserver) {
  // Batch updates that contain an actual write to the model should trigger
  // the observer.
  EXPECT_CALL(*observer(), ReadingListChanged());
  std::unique_ptr<ReadingListModel::ScopedReadingListBatchUpdate> update =
      reading_list_model()->BeginBatchUpdates();
  manager()->Add(GURL("https://google.com"), "google");
  update.reset();

  // Empty batch updates shouldn't trigger the observer.
  EXPECT_CALL(*observer(), ReadingListChanged()).Times(0);
  update = reading_list_model()->BeginBatchUpdates();
  update.reset();
}

}  // namespace