chromium/chrome/browser/ash/crosapi/browser_launcher_unittest.cc

// 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.

#include "chrome/browser/ash/crosapi/browser_launcher.h"

#include <optional>
#include <string>
#include <string_view>
#include <vector>

#include "ash/constants/ash_switches.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/posix/unix_domain_socket.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/test/scoped_command_line.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/crosapi/idle_service_ash.h"
#include "chrome/browser/ash/crosapi/test_crosapi_dependency_registry.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/ash/settings/scoped_cros_settings_test_helper.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "chromeos/ash/components/login/login_state/login_state.h"
#include "chromeos/ash/components/standalone_browser/lacros_selection.h"
#include "chromeos/ash/components/system/fake_statistics_provider.h"
#include "chromeos/ash/components/system/statistics_provider.h"
#include "chromeos/crosapi/cpp/crosapi_constants.h"
#include "chromeos/startup/startup_switches.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/fake_device_ownership_waiter.h"
#include "components/user_manager/scoped_user_manager.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/ui_base_features.h"

namespace crosapi {

namespace {
const char kPrimaryProfileEmail[] = "user@test";
}  // namespace

class BrowserLauncherTest : public testing::Test {
 public:
  BrowserLauncherTest() = default;

  void SetUp() override {
    fake_user_manager_.Reset(std::make_unique<ash::FakeChromeUserManager>());
    CHECK(profile_manager_.SetUp());

    // Settings required to create startup data.
    crosapi::IdleServiceAsh::DisableForTesting();
    ash::LoginState::Initialize();
    crosapi_manager_ = crosapi::CreateCrosapiManagerWithTestRegistry();
    ash::system::StatisticsProvider::SetTestProvider(
        &fake_statistics_provider_);

    browser_launcher_.set_device_ownership_waiter_for_testing(
        std::make_unique<user_manager::FakeDeviceOwnershipWaiter>());
  }

  void TearDown() override {
    crosapi_manager_.reset();
    ash::LoginState::Shutdown();
  }

 protected:
  BrowserLauncher* browser_launcher() { return &browser_launcher_; }

  void CreatePrimaryProfile() {
    const AccountId account_id(AccountId::FromUserEmail(kPrimaryProfileEmail));
    fake_user_manager_->AddUser(account_id);
    fake_user_manager_->LoginUser(account_id,
                                  /*set_profile_created_flag=*/false);
    profile_manager_.CreateTestingProfile(account_id.GetUserEmail());
    fake_user_manager_->SimulateUserProfileLoad(account_id);
  }

  void PrepareFilesToPreload(const base::FilePath& lacros_dir) {
    base::CreateDirectory(lacros_dir.Append("locales"));
    for (const auto& file_path : BrowserLauncher::GetPreloadFiles(lacros_dir)) {
      base::WriteFile(file_path, "dummy file");
    }
  }

 private:
  content::BrowserTaskEnvironment task_environment_;

  std::unique_ptr<crosapi::CrosapiManager> crosapi_manager_;
  ash::ScopedCrosSettingsTestHelper cros_settings_test_helper_;
  user_manager::TypedScopedUserManager<ash::FakeChromeUserManager>
      fake_user_manager_;
  TestingProfileManager profile_manager_{TestingBrowserProcess::GetGlobal()};

  // Required to create startup data.
  ash::system::ScopedFakeStatisticsProvider fake_statistics_provider_;

  BrowserLauncher browser_launcher_;
};

TEST_F(BrowserLauncherTest, AdditionalParametersForLaunchParams) {
  BrowserLauncher::LaunchParamsFromBackground params;
  params.lacros_additional_args.emplace_back("--switch1");
  params.lacros_additional_args.emplace_back("--switch2=value2");

  base::test::ScopedCommandLine scoped_command_line;
  base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
      ash::switches::kLacrosChromeAdditionalArgs, "--foo####--switch3=value3");
  base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
      ash::switches::kLacrosChromeAdditionalEnv, "foo1=bar2####switch4=value4");

  BrowserLauncher::LaunchParams parameters(
      base::CommandLine({"/bin/sleep", "30"}), base::LaunchOptionsForTest());
  browser_launcher()->SetUpAdditionalParametersForTesting(params, parameters);

  EXPECT_TRUE(parameters.command_line.HasSwitch("switch1"));
  EXPECT_TRUE(parameters.command_line.HasSwitch("foo"));
  EXPECT_EQ(parameters.command_line.GetSwitchValueASCII("switch2"), "value2");
  EXPECT_EQ(parameters.command_line.GetSwitchValueASCII("switch3"), "value3");

  EXPECT_EQ(parameters.options.environment["foo1"], "bar2");
  EXPECT_EQ(parameters.options.environment["switch4"], "value4");

  EXPECT_EQ(parameters.command_line.GetSwitches().size(), 4u);
  EXPECT_EQ(parameters.options.environment.size(), 2u);
}

TEST_F(BrowserLauncherTest, WithoutAdditionalParametersForCommandLine) {
  BrowserLauncher::LaunchParamsFromBackground params;
  base::test::ScopedCommandLine scoped_command_line;
  BrowserLauncher::LaunchParams parameters(
      base::CommandLine({"/bin/sleep", "30"}), base::LaunchOptionsForTest());
  parameters.command_line.RemoveSwitch(
      ash::switches::kLacrosChromeAdditionalArgs);
  browser_launcher()->SetUpAdditionalParametersForTesting(params, parameters);
  EXPECT_EQ(parameters.command_line.GetSwitches().size(), 0u);
  EXPECT_EQ(parameters.options.environment.size(), 0u);
}

// A --vmodule value provided via --lacros-chrome-additional-args is preserved.
TEST_F(BrowserLauncherTest, LacrosChromeAdditionalArgsVModule) {
  base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
      ash::switches::kLacrosChromeAdditionalArgs, "--vmodule=foo");

  base::ScopedFD dummy_fd1, dummy_fd2;
  base::CreateSocketPair(&dummy_fd1, &dummy_fd2);
  mojo::PlatformChannel dummy_channel;

  BrowserLauncher::LaunchParamsFromBackground bg_params;
  bg_params.logfd = std::move(dummy_fd1);
  BrowserLauncher::LaunchParams params =
      browser_launcher()->CreateLaunchParamsForTesting({}, bg_params, {}, {},
                                                       {}, dummy_channel, {});

  EXPECT_EQ(params.command_line.GetSwitchValueASCII("vmodule"), "foo");
}

TEST_F(BrowserLauncherTest, LaunchAndTriggerTerminate) {
  // We'll use a process just does nothing for 30 seconds, which is long
  // enough to stably exercise the test cases we have.
  BrowserLauncher::LaunchParams parameters(
      base::CommandLine({"/bin/sleep", "30"}), base::LaunchOptionsForTest());
  browser_launcher()->LaunchProcessForTesting(parameters);
  EXPECT_TRUE(browser_launcher()->IsProcessValid());
  EXPECT_TRUE(browser_launcher()->TriggerTerminate(/*exit_code=*/0));
  int exit_code;
  EXPECT_TRUE(
      browser_launcher()->GetProcessForTesting().WaitForExit(&exit_code));
  // -1 is expected as an `exit_code` because it is compulsorily terminated by
  // signal.
  EXPECT_EQ(exit_code, -1);

  // TODO(mayukoaiba): We should reset process in order to check by
  // EXPECT_FALSE(browser_launcher()->IsProcessValid()) whether
  // "TriggerTerminate" works properly.
}

TEST_F(BrowserLauncherTest, TerminateOnBackground) {
  // We'll use a process just does nothing for 30 seconds, which is long
  // enough to stably exercise the test cases we have.
  BrowserLauncher::LaunchParams parameters(
      base::CommandLine({"/bin/sleep", "30"}), base::LaunchOptionsForTest());
  browser_launcher()->LaunchProcessForTesting(parameters);
  ASSERT_TRUE(browser_launcher()->IsProcessValid());
  base::test::TestFuture<void> future;
  browser_launcher()->EnsureProcessTerminated(future.GetCallback(),
                                              base::Seconds(5));
  EXPECT_FALSE(browser_launcher()->IsProcessValid());
}

TEST_F(BrowserLauncherTest, BackgroundWorkPreLaunch) {
  base::ScopedTempDir lacros_dir;
  ASSERT_TRUE(lacros_dir.CreateUniqueTempDir());

  // Add feature and check if it's reflected to `params`.
  base::test::ScopedFeatureList scoped_features;
  scoped_features.InitAndEnableFeature(features::kLacrosResourcesFileSharing);

  BrowserLauncher::LaunchParamsFromBackground params;
  base::test::TestFuture<void> future;
  browser_launcher()->WaitForBackgroundWorkPreLaunchForTesting(
      lacros_dir.GetPath(), /*clear_shared_resource_file=*/true,
      /*launching_at_login_screen=*/false, future.GetCallback(), params);

  EXPECT_TRUE(future.Wait());
  EXPECT_TRUE(params.enable_resource_file_sharing);
}

TEST_F(BrowserLauncherTest, BackgroundWorkPreLaunchOnLaunchingAtLoginScreen) {
  base::ScopedTempDir lacros_dir;
  ASSERT_TRUE(lacros_dir.CreateUniqueTempDir());
  // Create preload files which will be loaded on launching at login screen.
  PrepareFilesToPreload(lacros_dir.GetPath());

  // Add feature and check if it's reflected to `params`.
  base::test::ScopedFeatureList scoped_features;
  scoped_features.InitAndEnableFeature(features::kLacrosResourcesFileSharing);

  BrowserLauncher::LaunchParamsFromBackground params;
  base::test::TestFuture<void> future;
  browser_launcher()->WaitForBackgroundWorkPreLaunchForTesting(
      lacros_dir.GetPath(), /*clear_shared_resource_file=*/true,
      /*launching_at_login_screen=*/true, future.GetCallback(), params);

  EXPECT_TRUE(future.Wait());
  EXPECT_TRUE(params.enable_resource_file_sharing);
}

// TODO(elkurin): Add kLacrosChromeAdditionalArgsFile unit test.

TEST_F(BrowserLauncherTest, Launch) {
  base::ScopedTempDir lacros_dir;
  ASSERT_TRUE(lacros_dir.CreateUniqueTempDir());
  // Create dummy lacros binary.
  const base::FilePath lacros_path = lacros_dir.GetPath().Append("chrome");
  base::WriteFile(lacros_path, "I am chrome binary");

  base::test::TestFuture<base::expected<BrowserLauncher::LaunchResults,
                                        BrowserLauncher::LaunchFailureReason>>
      future;

  constexpr bool launching_at_login_screen = false;
  browser_launcher()->Launch(lacros_path, launching_at_login_screen,
                             ash::standalone_browser::LacrosSelection::kRootfs,
                             /*mojo_disconneciton_cb=*/{},
                             /*is_keep_alive_enabled=*/false,
                             future.GetCallback());

  // Before adding primary profile, Launch should not proceed.
  EXPECT_FALSE(user_manager::UserManager::Get()->GetPrimaryUser());
  EXPECT_FALSE(future.IsReady());

  // Create primary profile.
  CreatePrimaryProfile();
  EXPECT_TRUE(user_manager::UserManager::Get()->GetPrimaryUser());

  // Make sure that Launch completes with success.
  EXPECT_TRUE(future.Get<0>().has_value());
}

TEST_F(BrowserLauncherTest, LaunchAtLoginScreen) {
  base::ScopedTempDir lacros_dir;
  ASSERT_TRUE(lacros_dir.CreateUniqueTempDir());
  // Create preload files which will be loaded on launching at login screen.
  // This will create chrome binary as well.
  PrepareFilesToPreload(lacros_dir.GetPath());
  const base::FilePath lacros_path = lacros_dir.GetPath().Append("chrome");

  base::test::TestFuture<base::expected<BrowserLauncher::LaunchResults,
                                        BrowserLauncher::LaunchFailureReason>>
      future;

  constexpr bool launching_at_login_screen = true;
  browser_launcher()->Launch(lacros_path, launching_at_login_screen,
                             ash::standalone_browser::LacrosSelection::kRootfs,
                             /*mojo_disconneciton_cb=*/{},
                             /*is_keep_alive_enabled=*/false,
                             future.GetCallback());

  // Make sure that Launch completes with success. In launching at login screen
  // scenario, we completes Launch flow without waiting for the primary profile.
  EXPECT_TRUE(future.Get<0>().has_value());
  EXPECT_FALSE(user_manager::UserManager::Get()->GetPrimaryUser());
}

TEST_F(BrowserLauncherTest, ResumeLaunch) {
  // Creates postlogin data pipe.
  base::ScopedFD read_postlogin_fd =
      browser_launcher()->CreatePostLoginPipeForTesting();
  ASSERT_TRUE(read_postlogin_fd.is_valid());

  // Creates a dedicated thread to read postlogin data which mocks Lacros
  // process.
  base::test::TestFuture<void> wait_read;
  base::ThreadPool::CreateSingleThreadTaskRunner(
      {base::MayBlock()}, base::SingleThreadTaskRunnerThreadMode::DEDICATED)
      ->PostTaskAndReply(
          FROM_HERE,
          base::BindOnce(
              [](base::ScopedFD fd) {
                base::ScopedFILE file(fdopen(fd.get(), "r"));
                std::string content;
                EXPECT_TRUE(base::ReadStreamToString(file.get(), &content));
                fd.reset();
              },
              std::move(read_postlogin_fd)),
          base::BindOnce(wait_read.GetCallback()));

  // Resume launch. This must be called after potlogin data pipe is constructed.
  base::test::TestFuture<
      base::expected<base::TimeTicks, BrowserLauncher::LaunchFailureReason>>
      future;
  browser_launcher()->ResumeLaunch(future.GetCallback());
  // On resume launch, we need to wait for device owner and primary profile to
  // be ready, so should not complete ResumeLaunch at this point.
  EXPECT_FALSE(future.IsReady());

  // Create primary profile.
  CreatePrimaryProfile();
  EXPECT_TRUE(user_manager::UserManager::Get()->GetPrimaryUser());

  // Make sure that ResumeLaunch complets with success.
  EXPECT_TRUE(future.Get<0>().has_value());

  // Wait for fd to close for clean up.
  EXPECT_TRUE(wait_read.Wait());
}

TEST_F(BrowserLauncherTest, ShutdownRequestedDuringLaunch) {
  base::test::TestFuture<base::expected<BrowserLauncher::LaunchResults,
                                        BrowserLauncher::LaunchFailureReason>>
      future;

  // To test asynchronous behavior, we assume it's not launching at login
  // screen.
  constexpr bool launching_at_login_screen = false;
  browser_launcher()->Launch(base::FilePath(), launching_at_login_screen,
                             ash::standalone_browser::LacrosSelection::kRootfs,
                             /*mojo_disconneciton_cb=*/{},
                             /*is_keep_alive_enabled=*/false,
                             future.GetCallback());
  // Shutdown is synchronous while Launch preparation is asynchronously waiting,
  // for primary profiel to be ready so Shutdown request runs earlier.
  browser_launcher()->Shutdown();

  // Create primary profile and proceed Launch.
  CreatePrimaryProfile();
  EXPECT_TRUE(user_manager::UserManager::Get()->GetPrimaryUser());

  // Launch should fail due to shutdown requested.
  EXPECT_FALSE(future.Get<0>().has_value());
  EXPECT_EQ(BrowserLauncher::LaunchFailureReason::kShutdownRequested,
            future.Get<0>().error());
}

}  // namespace crosapi