blob: 76c00c0426fef98ce8a5e62bb261897708fa6067 [file] [log] [blame]
// Copyright 2012 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/extensions/bookmarks/bookmarks_helpers.h"
#include <stdint.h>
#include <memory>
#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/bookmarks/managed_bookmark_service_factory.h"
#include "chrome/browser/extensions/bookmarks/bookmarks_error_constants.h"
#include "chrome/browser/sync/local_or_syncable_bookmark_sync_service_factory.h"
#include "chrome/common/extensions/api/bookmarks.h"
#include "chrome/test/base/testing_profile.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/managed/managed_bookmark_service.h"
#include "components/bookmarks/test/bookmark_test_helpers.h"
#include "components/bookmarks/test/test_bookmark_client.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/sync_bookmarks/bookmark_sync_service.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;
namespace extensions {
using api::bookmarks::BookmarkTreeNode;
using ::testing::Eq;
using ::testing::ExplainMatchResult;
namespace bookmarks_helpers {
// Matches a BookmarkTreeNode that represents a folder node with the given
// `expected_type` and `expected_unmodifiable`.
MATCHER_P2(MatchesFolder, expected_type, expected_unmodifiable, "") {
return ExplainMatchResult(Eq(expected_type), arg.folder_type,
result_listener) &&
ExplainMatchResult(Eq(expected_unmodifiable), arg.unmodifiable,
result_listener) &&
ExplainMatchResult(Eq(std::nullopt), arg.url, result_listener);
}
class ExtensionBookmarksTest : public testing::Test {
public:
ExtensionBookmarksTest()
: managed_(nullptr),
model_(nullptr),
node_(nullptr),
node2_(nullptr),
folder_(nullptr) {}
void SetUp() override {
TestingProfile::Builder profile_builder;
profile_builder.AddTestingFactory(
BookmarkModelFactory::GetInstance(),
BookmarkModelFactory::GetDefaultFactory());
profile_builder.AddTestingFactory(
ManagedBookmarkServiceFactory::GetInstance(),
ManagedBookmarkServiceFactory::GetDefaultFactory());
profile_ = profile_builder.Build();
model_ = BookmarkModelFactory::GetForBrowserContext(profile_.get());
managed_ = ManagedBookmarkServiceFactory::GetForProfile(profile_.get());
bookmarks::test::WaitForBookmarkModelToLoad(model_);
node_ = model_->AddURL(model_->other_node(), 0, u"Digg",
GURL("http://www.reddit.com"));
model_->SetNodeMetaInfo(node_, "some_key1", "some_value1");
model_->SetNodeMetaInfo(node_, "some_key2", "some_value2");
model_->AddURL(model_->other_node(), 0, u"News",
GURL("http://www.foxnews.com"));
folder_ = model_->AddFolder(model_->other_node(), 0, u"outer folder");
model_->SetNodeMetaInfo(folder_, "some_key1", "some_value1");
model_->AddFolder(folder_, 0, u"inner folder 1");
model_->AddFolder(folder_, 0, u"inner folder 2");
node2_ = model_->AddURL(folder_, 0, u"Digg", GURL("http://reddit.com"));
model_->SetNodeMetaInfo(node2_, "some_key2", "some_value2");
model_->AddURL(folder_, 0, u"CNet", GURL("http://cnet.com"));
// Add a URL to the mobile node so that it is not hidden.
model_->AddURL(model_->mobile_node(), 0, u"Mobile bookmark",
GURL("http://www.mobile.com"));
// Add a URL to the managed node so that it is not hidden.
model_->AddURL(managed_->managed_node(), 0, u"Managed bookmark",
GURL("http://www.managed.com"));
}
// A simple wrapper to get a single BookmarkTreeNode from a BookmarkNode (with
// no recursion).
BookmarkTreeNode GetSingleBookmarkTreeNode(const BookmarkNode* node) {
CHECK(node);
return GetBookmarkTreeNode(model_, managed_, node,
/*recurse=*/false,
/*only_folders=*/false);
}
content::BrowserTaskEnvironment task_environment_;
std::unique_ptr<TestingProfile> profile_;
raw_ptr<bookmarks::ManagedBookmarkService> managed_;
raw_ptr<BookmarkModel> model_;
raw_ptr<const BookmarkNode> node_;
raw_ptr<const BookmarkNode> node2_;
raw_ptr<const BookmarkNode> folder_;
};
TEST_F(ExtensionBookmarksTest, GetFullTreeFromRoot) {
BookmarkTreeNode tree =
GetBookmarkTreeNode(model_, managed_, model_->root_node(),
/*recurse=*/true,
/*only_folders=*/false);
ASSERT_EQ(4U, tree.children->size());
}
TEST_F(ExtensionBookmarksTest, GetTreeFromOtherPermanentNode) {
BookmarkTreeNode tree =
GetBookmarkTreeNode(model_, managed_, model_->other_node(),
/*recurse=*/true,
/*only_folders=*/false);
ASSERT_EQ(3U, tree.children->size());
}
TEST_F(ExtensionBookmarksTest, GetFoldersOnlyFromOtherPermanentNode) {
BookmarkTreeNode tree =
GetBookmarkTreeNode(model_, managed_, model_->other_node(),
/*recurse=*/true,
/*only_folders=*/true);
ASSERT_EQ(1U, tree.children->size());
}
TEST_F(ExtensionBookmarksTest, GetSubtree) {
BookmarkTreeNode tree = GetBookmarkTreeNode(model_, managed_, folder_,
/*recurse=*/true,
/*only_folders=*/false);
ASSERT_EQ(4U, tree.children->size());
const BookmarkTreeNode& digg = tree.children->at(1);
ASSERT_EQ("Digg", digg.title);
}
TEST_F(ExtensionBookmarksTest, GetSubtreeFoldersOnly) {
BookmarkTreeNode tree = GetBookmarkTreeNode(model_, managed_, folder_,
/*recurse=*/true,
/*only_folders=*/true);
ASSERT_EQ(2U, tree.children->size());
const BookmarkTreeNode& inner_folder = tree.children->at(1);
ASSERT_EQ("inner folder 1", inner_folder.title);
}
TEST_F(ExtensionBookmarksTest, GetModifiableNode) {
BookmarkTreeNode tree = GetBookmarkTreeNode(model_, managed_, node_,
/*recurse=*/false,
/*only_folders=*/false);
EXPECT_EQ("Digg", tree.title);
ASSERT_TRUE(tree.url);
EXPECT_EQ("http://www.reddit.com/", *tree.url);
EXPECT_EQ(api::bookmarks::BookmarkTreeNodeUnmodifiable::kNone,
tree.unmodifiable);
EXPECT_EQ(api::bookmarks::FolderType::kNone, tree.folder_type);
}
TEST_F(ExtensionBookmarksTest, GetManagedNode) {
const BookmarkNode* managed_bookmark =
model_->AddURL(managed_->managed_node(), 0, u"Chromium",
GURL("http://www.chromium.org/"));
BookmarkTreeNode tree =
GetBookmarkTreeNode(model_, managed_, managed_bookmark,
/*recurse=*/false,
/*only_folders=*/false);
EXPECT_EQ("Chromium", tree.title);
EXPECT_EQ("http://www.chromium.org/", *tree.url);
EXPECT_EQ(api::bookmarks::BookmarkTreeNodeUnmodifiable::kManaged,
tree.unmodifiable);
EXPECT_EQ(api::bookmarks::FolderType::kNone, tree.folder_type);
}
TEST_F(ExtensionBookmarksTest, GetLocalPermanentAndManagedFolders) {
EXPECT_THAT(
GetSingleBookmarkTreeNode(model_->bookmark_bar_node()),
MatchesFolder(api::bookmarks::FolderType::kBookmarksBar,
api::bookmarks::BookmarkTreeNodeUnmodifiable::kNone));
EXPECT_THAT(
GetSingleBookmarkTreeNode(model_->other_node()),
MatchesFolder(api::bookmarks::FolderType::kOther,
api::bookmarks::BookmarkTreeNodeUnmodifiable::kNone));
EXPECT_THAT(
GetSingleBookmarkTreeNode(model_->mobile_node()),
MatchesFolder(api::bookmarks::FolderType::kMobile,
api::bookmarks::BookmarkTreeNodeUnmodifiable::kNone));
EXPECT_THAT(
GetSingleBookmarkTreeNode(managed_->managed_node()),
MatchesFolder(api::bookmarks::FolderType::kManaged,
api::bookmarks::BookmarkTreeNodeUnmodifiable::kManaged));
}
TEST_F(ExtensionBookmarksTest, GetAccountPermanentNodes) {
base::test::ScopedFeatureList features{
switches::kSyncEnableBookmarksInTransportMode};
model_->CreateAccountPermanentFolders();
// Add a URL to the mobile node so that it is not hidden.
model_->AddURL(model_->account_mobile_node(), 0, u"Mobile bookmark",
GURL("http://www.mobile.com"));
EXPECT_THAT(
GetSingleBookmarkTreeNode(model_->account_bookmark_bar_node()),
MatchesFolder(api::bookmarks::FolderType::kBookmarksBar,
api::bookmarks::BookmarkTreeNodeUnmodifiable::kNone));
EXPECT_THAT(
GetSingleBookmarkTreeNode(model_->account_other_node()),
MatchesFolder(api::bookmarks::FolderType::kOther,
api::bookmarks::BookmarkTreeNodeUnmodifiable::kNone));
EXPECT_THAT(
GetSingleBookmarkTreeNode(model_->account_mobile_node()),
MatchesFolder(api::bookmarks::FolderType::kMobile,
api::bookmarks::BookmarkTreeNodeUnmodifiable::kNone));
}
TEST_F(ExtensionBookmarksTest,
GetTreePopulatesSyncingProperty_BookmarksInTransportModeDisabled) {
// Check that local permanent system nodes are not syncing.
EXPECT_FALSE(GetSingleBookmarkTreeNode(model_->bookmark_bar_node()).syncing);
EXPECT_FALSE(GetSingleBookmarkTreeNode(model_->other_node()).syncing);
EXPECT_FALSE(GetSingleBookmarkTreeNode(model_->mobile_node()).syncing);
EXPECT_FALSE(GetSingleBookmarkTreeNode(managed_->managed_node()).syncing);
// Check that non-permanent local nodes are not syncing.
EXPECT_FALSE(GetSingleBookmarkTreeNode(node_).syncing);
EXPECT_FALSE(GetSingleBookmarkTreeNode(folder_).syncing);
}
TEST_F(ExtensionBookmarksTest,
GetTreePopulatesSyncingProperty_SyncFeatureEnabledIncludingBookmarks) {
// Pretend sync-the-feature is on for bookmarks.
LocalOrSyncableBookmarkSyncServiceFactory::GetForProfile(profile_.get())
->SetIsTrackingMetadataForTesting();
// Check that local-or-syncable permanent nodes are syncing.
EXPECT_TRUE(GetSingleBookmarkTreeNode(model_->bookmark_bar_node()).syncing);
EXPECT_TRUE(GetSingleBookmarkTreeNode(model_->other_node()).syncing);
EXPECT_TRUE(GetSingleBookmarkTreeNode(model_->mobile_node()).syncing);
// Check that non-permanent local nodes are syncing.
EXPECT_TRUE(GetSingleBookmarkTreeNode(node_).syncing);
EXPECT_TRUE(GetSingleBookmarkTreeNode(folder_).syncing);
// Managed nodes are never syncing.
EXPECT_FALSE(GetSingleBookmarkTreeNode(managed_->managed_node()).syncing);
const BookmarkNode* managed_bookmark =
model_->AddURL(managed_->managed_node(), 0, u"Chromium",
GURL("http://www.chromium.org"));
EXPECT_FALSE(GetSingleBookmarkTreeNode(managed_bookmark).syncing);
}
TEST_F(ExtensionBookmarksTest,
GetTreePopulatesSyncingProperty_AccountNodesEnabled) {
base::test::ScopedFeatureList features{
switches::kSyncEnableBookmarksInTransportMode};
model_->CreateAccountPermanentFolders();
// Add a URL to the mobile node so that it is not hidden.
model_->AddURL(model_->account_mobile_node(), 0, u"Mobile bookmark",
GURL("http://www.mobile.com"));
// Check that local permanent nodes are not syncing.
EXPECT_FALSE(GetSingleBookmarkTreeNode(model_->bookmark_bar_node()).syncing);
EXPECT_FALSE(GetSingleBookmarkTreeNode(model_->other_node()).syncing);
EXPECT_FALSE(GetSingleBookmarkTreeNode(model_->mobile_node()).syncing);
EXPECT_FALSE(GetSingleBookmarkTreeNode(managed_->managed_node()).syncing);
// Check that non-permanent local nodes are not syncing.
EXPECT_FALSE(GetSingleBookmarkTreeNode(node_).syncing);
EXPECT_FALSE(GetSingleBookmarkTreeNode(folder_).syncing);
// Check that account permanent nodes are syncing.
EXPECT_TRUE(
GetSingleBookmarkTreeNode(model_->account_bookmark_bar_node()).syncing);
EXPECT_TRUE(GetSingleBookmarkTreeNode(model_->account_other_node()).syncing);
EXPECT_TRUE(GetSingleBookmarkTreeNode(model_->account_mobile_node()).syncing);
// Check that non-permanent account nodes are syncing.
const BookmarkNode* account_bookmark = model_->AddURL(
model_->account_other_node(), 0, u"Digg", GURL("http://www.reddit.com"));
const BookmarkNode* account_folder =
model_->AddFolder(model_->account_other_node(), 0, u"outer folder");
EXPECT_TRUE(GetSingleBookmarkTreeNode(account_bookmark).syncing);
EXPECT_TRUE(GetSingleBookmarkTreeNode(account_folder).syncing);
}
TEST_F(ExtensionBookmarksTest, RemoveNodeInvalidId) {
int64_t invalid_id = model_->next_node_id();
std::string error;
EXPECT_FALSE(RemoveNode(model_, managed_, invalid_id, true, &error));
EXPECT_EQ(bookmarks_errors::kNoNodeError, error);
}
TEST_F(ExtensionBookmarksTest, RemoveNodePermanent) {
std::string error;
EXPECT_FALSE(
RemoveNode(model_, managed_, model_->other_node()->id(), true, &error));
EXPECT_EQ(bookmarks_errors::kModifySpecialError, error);
}
TEST_F(ExtensionBookmarksTest, RemoveNodeManaged) {
const BookmarkNode* managed_bookmark =
model_->AddURL(managed_->managed_node(), 0, u"Chromium",
GURL("http://www.chromium.org"));
std::string error;
EXPECT_FALSE(
RemoveNode(model_, managed_, managed_bookmark->id(), true, &error));
EXPECT_EQ(bookmarks_errors::kModifyManagedError, error);
}
TEST_F(ExtensionBookmarksTest, RemoveNodeNotRecursive) {
std::string error;
EXPECT_FALSE(RemoveNode(model_, managed_, folder_->id(), false, &error));
EXPECT_EQ(bookmarks_errors::kFolderNotEmptyError, error);
}
TEST_F(ExtensionBookmarksTest, RemoveNodeRecursive) {
EXPECT_EQ(3u, model_->other_node()->children().size());
std::string error;
EXPECT_TRUE(RemoveNode(model_, managed_, folder_->id(), true, &error));
EXPECT_EQ(2u, model_->other_node()->children().size());
}
TEST_F(ExtensionBookmarksTest, GetMetaInfo) {
base::Value::Dict id_to_meta_info_map;
GetMetaInfo(*model_->other_node(), id_to_meta_info_map);
EXPECT_EQ(8u, id_to_meta_info_map.size());
// Verify top level node.
{
const base::Value* value = id_to_meta_info_map.Find(
base::NumberToString(model_->other_node()->id()));
ASSERT_NE(value, nullptr);
ASSERT_TRUE(value->is_dict());
const base::Value::Dict& dict = value->GetDict();
EXPECT_EQ(0u, dict.size());
}
// Verify bookmark with two meta info key/value pairs.
{
const base::Value* value =
id_to_meta_info_map.Find(base::NumberToString(node_->id()));
ASSERT_NE(value, nullptr);
ASSERT_TRUE(value->is_dict());
const base::Value::Dict& dict = value->GetDict();
EXPECT_EQ(2u, dict.size());
ASSERT_TRUE(dict.FindString("some_key1"));
EXPECT_EQ("some_value1", *(dict.FindString("some_key1")));
ASSERT_TRUE(dict.FindString("some_key2"));
EXPECT_EQ("some_value2", *(dict.FindString("some_key2")));
}
// Verify folder with one meta info key/value pair.
{
const base::Value* value =
id_to_meta_info_map.Find(base::NumberToString(folder_->id()));
ASSERT_NE(value, nullptr);
ASSERT_TRUE(value->is_dict());
const base::Value::Dict& dict = value->GetDict();
EXPECT_EQ(1u, dict.size());
ASSERT_TRUE(dict.FindString("some_key1"));
EXPECT_EQ("some_value1", *(dict.FindString("some_key1")));
}
// Verify bookmark in a subfolder with one meta info key/value pairs.
{
const base::Value* value =
id_to_meta_info_map.Find(base::NumberToString(node2_->id()));
ASSERT_NE(value, nullptr);
ASSERT_TRUE(value->is_dict());
const base::Value::Dict& dict = value->GetDict();
EXPECT_EQ(1u, dict.size());
ASSERT_FALSE(dict.FindString("some_key1"));
ASSERT_TRUE(dict.FindString("some_key2"));
EXPECT_EQ("some_value2", *(dict.FindString("some_key2")));
}
}
TEST_F(ExtensionBookmarksTest, PopulateBookmarkTreeNodeIndexConsistency) {
const BookmarkNode* url1 = model_->AddURL(model_->bookmark_bar_node(), 0,
u"URL1", GURL("http://url1.com"));
const BookmarkNode* url2 = model_->AddURL(model_->bookmark_bar_node(), 1,
u"URL2", GURL("http://url2.com"));
const BookmarkNode* subfolder =
model_->AddFolder(model_->bookmark_bar_node(), 2, u"Subfolder");
const BookmarkNode* url3 =
model_->AddURL(subfolder, 0, u"URL3", GURL("http://url3.com"));
// Get the tree and verify indexes.
BookmarkTreeNode tree =
GetBookmarkTreeNode(model_, managed_, model_->bookmark_bar_node(),
/*recurse=*/true, /*only_folders=*/false);
ASSERT_TRUE(tree.children.has_value());
const std::vector<BookmarkTreeNode>& children = *tree.children;
// Helper to find node by title.
auto find_node = [](const std::vector<BookmarkTreeNode>& nodes,
const std::string& title) {
return std::find_if(
nodes.begin(), nodes.end(),
[&title](const BookmarkTreeNode& node) { return node.title == title; });
};
// Verify each node's visible_index matches GetAPIIndexOf.
auto url1_it = find_node(children, "URL1");
ASSERT_NE(url1_it, children.end());
EXPECT_EQ(GetAPIIndexOf(*url1), url1_it->index);
auto url2_it = find_node(children, "URL2");
ASSERT_NE(url2_it, children.end());
EXPECT_EQ(GetAPIIndexOf(*url2), url2_it->index);
auto subfolder_it = find_node(children, "Subfolder");
ASSERT_NE(subfolder_it, children.end());
EXPECT_EQ(GetAPIIndexOf(*subfolder), subfolder_it->index);
ASSERT_TRUE(subfolder_it->children.has_value());
auto url3_it = find_node(*subfolder_it->children, "URL3");
ASSERT_NE(url3_it, subfolder_it->children->end());
EXPECT_EQ(GetAPIIndexOf(*url3), url3_it->index);
// Verify indexes reflect the node order.
EXPECT_EQ(0U, url1_it->index);
EXPECT_EQ(1U, url2_it->index);
EXPECT_EQ(2U, subfolder_it->index);
EXPECT_EQ(0U, url3_it->index);
// Directly test PopulateBookmarkTreeNode.
api::bookmarks::BookmarkTreeNode populated_node;
PopulateBookmarkTreeNode(model_, managed_, url1,
/*recurse=*/false, /*only_folders=*/false,
std::nullopt, &populated_node);
EXPECT_EQ(GetAPIIndexOf(*url1), populated_node.index);
// Test recursive population.
api::bookmarks::BookmarkTreeNode recursive_node;
PopulateBookmarkTreeNode(model_, managed_, subfolder,
/*recurse=*/true, /*only_folders=*/false,
std::nullopt, &recursive_node);
EXPECT_EQ(GetAPIIndexOf(*subfolder), recursive_node.index);
ASSERT_TRUE(recursive_node.children.has_value());
ASSERT_EQ(1U, recursive_node.children->size());
EXPECT_EQ(GetAPIIndexOf(*url3), recursive_node.children->at(0).index);
// Test only_folders with recursion.
api::bookmarks::BookmarkTreeNode folders_node;
PopulateBookmarkTreeNode(model_, managed_, model_->bookmark_bar_node(),
/*recurse=*/true, /*only_folders=*/true,
std::nullopt, &folders_node);
ASSERT_TRUE(folders_node.children.has_value());
ASSERT_EQ(1U, folders_node.children->size()); // Only the subfolder.
EXPECT_EQ(GetAPIIndexOf(*subfolder), folders_node.children->at(0).index);
}
} // namespace bookmarks_helpers
} // namespace extensions