blob: c3839a919ee6757f9a90c723388a137619c1de69 [file] [log] [blame]
// Copyright 2011 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/first_run/first_run.h"
#include <utility>
#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/path_service.h"
#include "base/task/current_thread.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_path_override.h"
#include "base/test/test_future.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/first_run/first_run_internal.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/installer/util/initial_preferences.h"
#include "chrome/test/base/testing_profile.h"
#include "components/bookmarks/browser/base_bookmark_model_observer.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/startup_metric_utils/browser/startup_metric_utils.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
using first_run::internal::FirstRunImportBookmarksResult;
namespace first_run {
namespace {
base::FilePath GetTestDataPath(const std::string& test_name) {
return base::PathService::CheckedGet(chrome::DIR_TEST_DATA)
.AppendASCII("first_run")
.AppendASCII(test_name);
}
base::FilePath GetSentinelFilePath() {
return base::PathService::CheckedGet(chrome::DIR_USER_DATA)
.Append(chrome::kFirstRunSentinel);
}
base::Value::Dict ParseJSONIfValid(std::string_view json) {
std::optional<base::Value::Dict> parsed_json =
base::JSONReader::ReadDict(json);
if (!parsed_json.has_value()) {
ADD_FAILURE() << "JSON parsing failed";
}
return *std::move(parsed_json);
}
// An observer that waits for the extensive changes on the bookmark model to
// end, which signals the completion of the bookmark import.
class BookmarkImportObserver final
: public bookmarks::BaseBookmarkModelObserver {
public:
explicit BookmarkImportObserver(base::OnceClosure quit_closure)
: quit_closure_(std::move(quit_closure)) {}
void ExtensiveBookmarkChangesEnded() override {
std::move(quit_closure_).Run();
}
void BookmarkModelChanged() override {}
void BookmarkModelBeingDeleted() override {
if (quit_closure_) {
ADD_FAILURE() << "Bookmark model deleted before import completed.";
std::move(quit_closure_).Run();
}
}
private:
base::OnceClosure quit_closure_;
};
} // namespace
class FirstRunTest : public testing::Test {
protected:
void TearDown() override {
first_run::ResetCachedSentinelDataForTesting();
Test::TearDown();
}
private:
base::ScopedPathOverride user_data_dir_override_{chrome::DIR_USER_DATA};
};
TEST_F(FirstRunTest, SetupInitialPrefsFromInstallPrefs_NoVariationsSeed) {
installer::InitialPreferences install_prefs("{ }");
EXPECT_TRUE(install_prefs.initial_dictionary().empty());
EXPECT_TRUE(install_prefs.GetCompressedVariationsSeed().empty());
EXPECT_TRUE(install_prefs.GetVariationsSeedSignature().empty());
}
TEST_F(FirstRunTest,
SetupInitialPrefsFromInstallPrefs_VariationsSeedSignature) {
installer::InitialPreferences install_prefs(
"{\"variations_compressed_seed\":\"xyz\","
" \"variations_seed_signature\":\"abc\"}");
EXPECT_EQ(2U, install_prefs.initial_dictionary().size());
EXPECT_EQ("xyz", install_prefs.GetCompressedVariationsSeed());
EXPECT_EQ("abc", install_prefs.GetVariationsSeedSignature());
// Variations prefs should have been extracted (removed) from the dictionary.
EXPECT_TRUE(install_prefs.initial_dictionary().empty());
}
// No switches and no sentinel present. This is the standard case for first run.
TEST_F(FirstRunTest, DetermineFirstRunState_FirstRun) {
internal::FirstRunState result =
internal::DetermineFirstRunState(false, false, false);
EXPECT_EQ(internal::FIRST_RUN_TRUE, result);
}
// Force switch is present, overriding both sentinel and suppress switch.
TEST_F(FirstRunTest, DetermineFirstRunState_ForceSwitch) {
internal::FirstRunState result =
internal::DetermineFirstRunState(true, true, true);
EXPECT_EQ(internal::FIRST_RUN_TRUE, result);
result = internal::DetermineFirstRunState(true, true, false);
EXPECT_EQ(internal::FIRST_RUN_TRUE, result);
result = internal::DetermineFirstRunState(false, true, true);
EXPECT_EQ(internal::FIRST_RUN_TRUE, result);
result = internal::DetermineFirstRunState(false, true, false);
EXPECT_EQ(internal::FIRST_RUN_TRUE, result);
}
// No switches, but sentinel present. This is not a first run.
TEST_F(FirstRunTest, DetermineFirstRunState_NotFirstRun) {
internal::FirstRunState result =
internal::DetermineFirstRunState(true, false, false);
EXPECT_EQ(internal::FIRST_RUN_FALSE, result);
}
// Suppress switch is present, overriding sentinel state.
TEST_F(FirstRunTest, DetermineFirstRunState_SuppressSwitch) {
internal::FirstRunState result =
internal::DetermineFirstRunState(false, false, true);
EXPECT_EQ(internal::FIRST_RUN_FALSE, result);
result = internal::DetermineFirstRunState(true, false, true);
EXPECT_EQ(internal::FIRST_RUN_FALSE, result);
}
TEST_F(FirstRunTest, GetFirstRunSentinelCreationTime_Created) {
base::HistogramTester histogram_tester;
first_run::CreateSentinelIfNeeded();
histogram_tester.ExpectUniqueSample(
"FirstRun.Sentinel.Created",
startup_metric_utils::FirstRunSentinelCreationResult::kSuccess, 1);
// Gets the creation time of the first run sentinel.
base::File::Info info;
ASSERT_TRUE(base::GetFileInfo(GetSentinelFilePath(), &info));
EXPECT_EQ(info.creation_time, first_run::GetFirstRunSentinelCreationTime());
}
TEST_F(FirstRunTest, GetFirstRunSentinelCreationTime_NotCreated) {
base::File::Info info;
ASSERT_FALSE(base::GetFileInfo(GetSentinelFilePath(), &info));
EXPECT_EQ(
0,
first_run::GetFirstRunSentinelCreationTime().InSecondsFSinceUnixEpoch());
}
TEST_F(FirstRunTest, CreateSentinelIfNeeded) {
{
base::HistogramTester histogram_tester;
EXPECT_FALSE(base::PathExists(GetSentinelFilePath()));
EXPECT_TRUE(IsChromeFirstRun());
first_run::CreateSentinelIfNeeded();
histogram_tester.ExpectUniqueSample(
"FirstRun.Sentinel.Created",
startup_metric_utils::FirstRunSentinelCreationResult::kSuccess, 1);
}
{
base::HistogramTester histogram_tester;
EXPECT_TRUE(base::PathExists(GetSentinelFilePath()));
EXPECT_TRUE(IsChromeFirstRun());
first_run::CreateSentinelIfNeeded();
// We are still considered in the first run, but we'll attempt a creation
// even if the file exists.
histogram_tester.ExpectUniqueSample(
"FirstRun.Sentinel.Created",
startup_metric_utils::FirstRunSentinelCreationResult::kFilePathExists,
1);
}
first_run::ResetCachedSentinelDataForTesting();
{
base::HistogramTester histogram_tester;
EXPECT_TRUE(base::PathExists(GetSentinelFilePath()));
EXPECT_FALSE(IsChromeFirstRun());
first_run::CreateSentinelIfNeeded();
// The file already exists, and we identified that we are not in the first
// run, the creation is not needed.
histogram_tester.ExpectTotalCount("FirstRun.Sentinel.Created", 0);
}
}
TEST_F(FirstRunTest, CreateSentinelIfNeeded_DoneEvenIfForced) {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kForceFirstRun);
{
base::HistogramTester histogram_tester;
EXPECT_FALSE(base::PathExists(GetSentinelFilePath()));
EXPECT_TRUE(IsChromeFirstRun());
first_run::CreateSentinelIfNeeded();
histogram_tester.ExpectUniqueSample(
"FirstRun.Sentinel.Created",
startup_metric_utils::FirstRunSentinelCreationResult::kSuccess, 1);
}
{
base::HistogramTester histogram_tester;
EXPECT_TRUE(base::PathExists(GetSentinelFilePath()));
EXPECT_TRUE(IsChromeFirstRun());
first_run::CreateSentinelIfNeeded();
// While the first run state is forced, we'll always attempt to create the
// sentinel.
histogram_tester.ExpectUniqueSample(
"FirstRun.Sentinel.Created",
startup_metric_utils::FirstRunSentinelCreationResult::kFilePathExists,
1);
}
first_run::ResetCachedSentinelDataForTesting();
{
base::HistogramTester histogram_tester;
EXPECT_TRUE(base::PathExists(GetSentinelFilePath()));
EXPECT_TRUE(IsChromeFirstRun());
first_run::CreateSentinelIfNeeded();
// While the first run state is forced, we'll always attempt to create the
// sentinel.
histogram_tester.ExpectUniqueSample(
"FirstRun.Sentinel.Created",
startup_metric_utils::FirstRunSentinelCreationResult::kFilePathExists,
1);
}
}
TEST_F(FirstRunTest, CreateSentinelIfNeeded_SkippedIfSuppressed) {
base::HistogramTester histogram_tester;
base::CommandLine::ForCurrentProcess()->AppendSwitch(switches::kNoFirstRun);
first_run::CreateSentinelIfNeeded();
histogram_tester.ExpectTotalCount("FirstRun.Sentinel.Created", 0);
EXPECT_FALSE(base::PathExists(GetSentinelFilePath()));
EXPECT_FALSE(IsChromeFirstRun());
}
#if BUILDFLAG(IS_POSIX) // This test relies on Posix file permissions.
TEST_F(FirstRunTest, CreateSentinelIfNeeded_FileSystemError) {
base::HistogramTester histogram_tester;
// Make the user data dir read-only so the sentinel can't be written.
// Note: the test fixture registers an override to a temp dir for the
// scope of each test, the below is not as destructive as it seems.
auto path = base::PathService::CheckedGet(chrome::DIR_USER_DATA);
ASSERT_TRUE(SetPosixFilePermissions(
path, DirectoryExists(path) ? (S_IRUSR | S_IXUSR) : S_IRUSR));
first_run::CreateSentinelIfNeeded();
histogram_tester.ExpectUniqueSample(
"FirstRun.Sentinel.Created",
startup_metric_utils::FirstRunSentinelCreationResult::kFileSystemError,
1);
EXPECT_FALSE(base::PathExists(GetSentinelFilePath()));
EXPECT_TRUE(IsChromeFirstRun()); // This is still a first run.
}
#endif
// This test, and the one below, require customizing the path that the initial
// prefs code will search. On non-Mac platforms that path is derived from
// PathService, but on macOS it instead comes from the system analog of
// PathService (NSSearchPathForDirectoriesInDomains), which we don't have an
// analogous scoped override for.
//
// TODO(ellyjones): Add a scoped override for
// NSSearchPathForDirectoriesInDomains, then re-enable these on macOS.
#if BUILDFLAG(IS_MAC)
#define MAYBE_InitialPrefsUsedIfReadable DISABLED_InitialPrefsUsedIfReadable
#else
#define MAYBE_InitialPrefsUsedIfReadable InitialPrefsUsedIfReadable
#endif
TEST_F(FirstRunTest, MAYBE_InitialPrefsUsedIfReadable) {
base::ScopedPathOverride override(base::DIR_EXE, GetTestDataPath("initial"));
std::unique_ptr<installer::InitialPreferences> prefs =
first_run::LoadInitialPrefs();
ASSERT_TRUE(prefs);
EXPECT_EQ(prefs->GetFirstRunTabs()[0], "https://www.chromium.org/initial");
}
#if BUILDFLAG(IS_MAC)
#define MAYBE_LegacyInitialPrefsUsedIfNewFileIsNotPresent \
DISABLED_LegacyInitialPrefsUsedIfNewFileIsNotPresent
#else
#define MAYBE_LegacyInitialPrefsUsedIfNewFileIsNotPresent \
LegacyInitialPrefsUsedIfNewFileIsNotPresent
#endif
TEST_F(FirstRunTest, MAYBE_LegacyInitialPrefsUsedIfNewFileIsNotPresent) {
base::ScopedPathOverride override(base::DIR_EXE, GetTestDataPath("legacy"));
std::unique_ptr<installer::InitialPreferences> prefs =
first_run::LoadInitialPrefs();
ASSERT_TRUE(prefs);
EXPECT_EQ(prefs->GetFirstRunTabs()[0], "https://www.chromium.org/legacy");
}
class FirstRunBookmarkImportTest : public testing::Test {
protected:
FirstRunBookmarkImportTest() {
TestingProfile::Builder builder;
builder.AddTestingFactory(BookmarkModelFactory::GetInstance(),
BookmarkModelFactory::GetDefaultFactory());
profile_ = builder.Build();
}
TestingProfile* profile() { return profile_.get(); }
private:
content::BrowserTaskEnvironment task_environment_;
std::unique_ptr<TestingProfile> profile_;
};
TEST_F(FirstRunBookmarkImportTest, SucceedsWithValidDict) {
base::Value::Dict bookmarks_dict = ParseJSONIfValid(
R"(
{
"first_run_bookmarks": {
"children": [
{
"name": "Google",
"type": "url",
"url": "https://www.google.com"
},
{
"name": "My Folder",
"type": "folder",
"children": [
{
"name": "YouTube",
"type": "url",
"url": "https://www.youtube.com"
}
]
}
]
}
}
)");
base::HistogramTester histogram_tester;
bookmarks::BookmarkModel* bookmark_model =
BookmarkModelFactory::GetForBrowserContext(profile());
base::test::TestFuture<void> future;
BookmarkImportObserver observer(future.GetCallback());
bookmark_model->AddObserver(&observer);
StartBookmarksImportFromDict(profile(), std::move(bookmarks_dict));
ASSERT_TRUE(future.Wait());
bookmark_model->RemoveObserver(&observer);
const bookmarks::BookmarkNode* bar = bookmark_model->bookmark_bar_node();
ASSERT_EQ(2u, bar->children().size());
const bookmarks::BookmarkNode* node1 = bar->children()[0].get();
EXPECT_EQ(u"Google", node1->GetTitle());
EXPECT_EQ(GURL("https://www.google.com"), node1->url());
const bookmarks::BookmarkNode* node2 = bar->children()[1].get();
EXPECT_EQ(u"My Folder", node2->GetTitle());
ASSERT_TRUE(node2->is_folder());
ASSERT_EQ(1u, node2->children().size());
const bookmarks::BookmarkNode* node3 = node2->children()[0].get();
EXPECT_EQ(u"YouTube", node3->GetTitle());
EXPECT_EQ(GURL("https://www.youtube.com"), node3->url());
histogram_tester.ExpectUniqueSample("FirstRun.ImportBookmarksDict",
FirstRunImportBookmarksResult::kSuccess,
1);
}
TEST_F(FirstRunBookmarkImportTest, FailsWithInvalidDict) {
base::Value::Dict bookmarks_dict =
ParseJSONIfValid(R"({"invalid_key": "invalid_value"})");
base::HistogramTester histogram_tester;
bookmarks::BookmarkModel* bookmark_model =
BookmarkModelFactory::GetForBrowserContext(profile());
StartBookmarksImportFromDict(profile(), std::move(bookmarks_dict));
// Since BookmarkModel::BeginExtensiveChanges/EndExtensiveChanges will not be
// triggered.
ASSERT_TRUE(base::test::RunUntil([&]() {
return histogram_tester.GetBucketCount(
"FirstRun.ImportBookmarksDict",
FirstRunImportBookmarksResult::kInvalidDict) > 0;
})) << "Timeout waiting for import to fail";
const bookmarks::BookmarkNode* bar = bookmark_model->bookmark_bar_node();
ASSERT_EQ(0u, bar->children().size());
histogram_tester.ExpectUniqueSample(
"FirstRun.ImportBookmarksDict",
FirstRunImportBookmarksResult::kInvalidDict, 1);
}
TEST_F(FirstRunBookmarkImportTest, FailsIfBookmarkModelIsMissing) {
TestingProfile::Builder builder;
// Do not install the BookmarkModelFactory.
std::unique_ptr<TestingProfile> profile_without_bookmark_model =
builder.Build();
base::HistogramTester histogram_tester;
StartBookmarksImportFromDict(profile_without_bookmark_model.get(),
base::Value::Dict());
histogram_tester.ExpectUniqueSample(
"FirstRun.ImportBookmarksDict",
FirstRunImportBookmarksResult::kInvalidProfile, 1);
}
TEST_F(FirstRunBookmarkImportTest, SucceedsWithSomeMalformedNodes) {
base::Value::Dict bookmarks_dict = ParseJSONIfValid(
R"({
"first_run_bookmarks": {
"children": [
{
"name": "Invalid",
"type": "url",
"url": "invalid-url"
},
{
"name": "Valid",
"type": "url",
"url": "https://www.validurl.com"
},
{
"name": "Missing Type",
"url": "https://www.example.com"
},
{
"type": "url",
"url": "https://www.example.com"
},
{
"name": "Empty Url",
"type": "url"
},
{
"name": 123,
"type": "url",
"url": "https://www.invalidnamewithnumbers.com"
},
{
"name": "Incorrect type for type",
"type": 123,
"url": "https://www.example.com"
},
{
"name": "Incorrect type for url",
"type": "url",
"url": 123
}
]
}
})");
base::HistogramTester histogram_tester;
bookmarks::BookmarkModel* bookmark_model =
BookmarkModelFactory::GetForBrowserContext(profile());
base::test::TestFuture<void> future;
BookmarkImportObserver observer(future.GetCallback());
bookmark_model->AddObserver(&observer);
StartBookmarksImportFromDict(profile(), std::move(bookmarks_dict));
ASSERT_TRUE(future.Wait());
bookmark_model->RemoveObserver(&observer);
const bookmarks::BookmarkNode* bar = bookmark_model->bookmark_bar_node();
ASSERT_EQ(1u, bar->children().size());
const bookmarks::BookmarkNode* node2 = bar->children()[0].get();
EXPECT_EQ(u"Valid", node2->GetTitle());
EXPECT_EQ(GURL("https://www.validurl.com"), node2->url());
histogram_tester.ExpectUniqueSample("FirstRun.ImportBookmarksDict",
FirstRunImportBookmarksResult::kSuccess,
1);
}
TEST_F(FirstRunBookmarkImportTest, SucceedsWithMalformedFolders) {
base::Value::Dict bookmarks_dict = ParseJSONIfValid(
R"({
"first_run_bookmarks": {
"children": [
{
"name": "Malformed Folder 1",
"type": "folder"
},
{
"name": "Malformed Folder 2",
"type": "folder",
"children": "not-a-list"
}
]
}
})");
base::HistogramTester histogram_tester;
bookmarks::BookmarkModel* bookmark_model =
BookmarkModelFactory::GetForBrowserContext(profile());
base::test::TestFuture<void> future;
BookmarkImportObserver observer(future.GetCallback());
bookmark_model->AddObserver(&observer);
StartBookmarksImportFromDict(profile(), std::move(bookmarks_dict));
ASSERT_TRUE(future.Wait());
bookmark_model->RemoveObserver(&observer);
const bookmarks::BookmarkNode* bar = bookmark_model->bookmark_bar_node();
ASSERT_EQ(2u, bar->children().size());
const bookmarks::BookmarkNode* node1 = bar->children()[0].get();
EXPECT_EQ(u"Malformed Folder 1", node1->GetTitle());
EXPECT_TRUE(node1->is_folder());
EXPECT_EQ(0u, node1->children().size());
const bookmarks::BookmarkNode* node2 = bar->children()[1].get();
EXPECT_EQ(u"Malformed Folder 2", node2->GetTitle());
EXPECT_TRUE(node2->is_folder());
EXPECT_EQ(0u, node2->children().size());
histogram_tester.ExpectUniqueSample("FirstRun.ImportBookmarksDict",
FirstRunImportBookmarksResult::kSuccess,
1);
}
} // namespace first_run