chromium/chrome/browser/ash/bruschetta/bruschetta_launcher_unittest.cc

// Copyright 2022 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/bruschetta/bruschetta_launcher.h"

#include <memory>
#include <optional>

#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "chrome/browser/ash/bruschetta/bruschetta_pref_names.h"
#include "chrome/browser/ash/bruschetta/bruschetta_service.h"
#include "chrome/browser/ash/bruschetta/bruschetta_util.h"
#include "chrome/browser/ash/guest_os/dbus_test_helper.h"
#include "chrome/browser/ash/guest_os/guest_os_session_tracker.h"
#include "chrome/browser/ash/guest_os/public/types.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/dbus/concierge/fake_concierge_client.h"
#include "chromeos/ash/components/dbus/dlcservice/dlcservice.pb.h"
#include "chromeos/ash/components/dbus/dlcservice/fake_dlcservice_client.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {
const char kLaunchHistogram[] = "Bruschetta.LaunchResult";
const char kTestVmName[] = "vm_name";
const char kTestVmConfig[] = "vm_config";
}

namespace bruschetta {

class BruschettaLauncherTest : public testing::Test,
                               protected guest_os::FakeVmServicesHelper {
 public:
  BruschettaLauncherTest() = default;
  BruschettaLauncherTest(const BruschettaLauncherTest&) = delete;
  BruschettaLauncherTest& operator=(const BruschettaLauncherTest&) = delete;
  ~BruschettaLauncherTest() override = default;

 protected:
  void SetUp() override {
    launcher_ = std::make_unique<BruschettaLauncher>(kTestVmName, &profile_);

    // We set up all our mocks to succeed, then failing tests explicitly break
    // the one thing they want to check the failure mode of.
    vm_tools::concierge::StartVmResponse response;
    response.set_success(true);
    response.set_status(vm_tools::concierge::VmStatus::VM_STATUS_RUNNING);
    FakeConciergeClient()->set_start_vm_response(std::move(response));

    guest_os::GuestId id{guest_os::VmType::BRUSCHETTA, kTestVmName, "penguin"};
    guest_os::GuestOsSessionTracker::GetForProfile(&profile_)
        ->AddGuestForTesting(id, guest_os::GuestInfo(id, 30, {}, {}, {}, {}));

    SetupPrefs();
  }

  void TearDown() override {}

  base::RepeatingCallback<void(BruschettaResult)> StoreResultThenQuitRunLoop(
      BruschettaResult* out_result) {
    return base::BindLambdaForTesting(
        [this, out_result](BruschettaResult result) {
          *out_result = result;
          this->run_loop_.Quit();
        });
  }

  void SetupPrefs() {
    BruschettaService::GetForProfile(&profile_)->RegisterInPrefs(
        MakeBruschettaId(kTestVmName), kTestVmConfig);

    base::Value::Dict pref;
    base::Value::Dict config;
    config.Set(prefs::kPolicyEnabledKey,
               static_cast<int>(prefs::PolicyEnabledState::RUN_ALLOWED));
    config.Set(prefs::kPolicyNameKey, "Display Name");

    base::Value::Dict vtpm;
    vtpm.Set(prefs::kPolicyVTPMEnabledKey, true);
    vtpm.Set(prefs::kPolicyVTPMUpdateActionKey,
             static_cast<int>(
                 prefs::PolicyUpdateAction::FORCE_SHUTDOWN_IF_MORE_RESTRICTED));

    config.Set(prefs::kPolicyVTPMKey, std::move(vtpm));

    pref.Set(kTestVmConfig, std::move(config));
    profile_.GetPrefs()->SetDict(prefs::kBruschettaVMConfiguration,
                                 std::move(pref));
  }

  void SetVtpmStatus(bool enabled) {
    ScopedDictPrefUpdate updater(profile_.GetPrefs(),
                                 prefs::kBruschettaVMConfiguration);

    updater.Get()
        .FindDict(kTestVmConfig)
        ->FindDict(prefs::kPolicyVTPMKey)
        ->Set(prefs::kPolicyVTPMEnabledKey, enabled);
  }

  content::BrowserTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  base::RunLoop run_loop_;
  TestingProfile profile_;
  std::unique_ptr<BruschettaLauncher> launcher_;
  base::HistogramTester histogram_tester_{};
};

// Try to launch, but DLC service returns an error.
TEST_F(BruschettaLauncherTest, LaunchToolsDlcFailure) {
  BruschettaResult result;
  FakeDlcserviceClient()->set_install_errors(base::circular_deque(
      {std::string("Error installing"), std::string(dlcservice::kErrorNone)}));

  launcher_->EnsureRunning(StoreResultThenQuitRunLoop(&result));
  run_loop_.Run();

  ASSERT_EQ(result, BruschettaResult::kDlcInstallError);
  histogram_tester_.ExpectUniqueSample(kLaunchHistogram,
                                       BruschettaResult::kDlcInstallError, 1);

  ASSERT_FALSE(BruschettaService::GetForProfile(&profile_)
                   ->GetRunningVmsForTesting()
                   .contains(kTestVmName));
}

// Try to launch, but DLC service returns an error.
TEST_F(BruschettaLauncherTest, LaunchFirmwareDlcFailure) {
  BruschettaResult result;
  FakeDlcserviceClient()->set_install_errors(base::circular_deque(
      {std::string(dlcservice::kErrorNone), std::string("Error installing")}));

  launcher_->EnsureRunning(StoreResultThenQuitRunLoop(&result));
  run_loop_.Run();

  ASSERT_EQ(result, BruschettaResult::kDlcInstallError);
  histogram_tester_.ExpectUniqueSample(kLaunchHistogram,
                                       BruschettaResult::kDlcInstallError, 1);

  ASSERT_FALSE(BruschettaService::GetForProfile(&profile_)
                   ->GetRunningVmsForTesting()
                   .contains(kTestVmName));
}

// Try to launch, but StartVm fails.
TEST_F(BruschettaLauncherTest, LaunchStartVmFails) {
  BruschettaResult result;
  vm_tools::concierge::StartVmResponse response;
  response.set_failure_reason("failure reason");
  response.set_success(false);
  response.set_status(vm_tools::concierge::VmStatus::VM_STATUS_FAILURE);
  FakeConciergeClient()->set_start_vm_response(std::move(response));

  launcher_->EnsureRunning(StoreResultThenQuitRunLoop(&result));
  run_loop_.Run();

  ASSERT_EQ(result, BruschettaResult::kStartVmFailed);
  histogram_tester_.ExpectUniqueSample(kLaunchHistogram,
                                       BruschettaResult::kStartVmFailed, 1);

  ASSERT_FALSE(BruschettaService::GetForProfile(&profile_)
                   ->GetRunningVmsForTesting()
                   .contains(kTestVmName));
}

// Try to launch, VM already running.
TEST_F(BruschettaLauncherTest, LaunchStartVmSuccess) {
  BruschettaResult result;
  vm_tools::concierge::StartVmResponse response;
  response.set_success(true);
  response.set_status(vm_tools::concierge::VmStatus::VM_STATUS_RUNNING);
  FakeConciergeClient()->set_start_vm_response(std::move(response));

  launcher_->EnsureRunning(StoreResultThenQuitRunLoop(&result));
  run_loop_.Run();

  ASSERT_EQ(result, BruschettaResult::kSuccess);

  // Alpha VMs should have vtpm enabled.
  const auto& running_vms =
      BruschettaService::GetForProfile(&profile_)->GetRunningVmsForTesting();
  auto it = running_vms.find(kTestVmName);
  ASSERT_NE(it, running_vms.end());
  ASSERT_TRUE(it->second.vtpm_enabled);

  // Run for another few minutes to check that we only get the single success
  // metric and not e.g. a spurious timeout metric as we saw in b/299415527.
  this->task_environment_.FastForwardBy(base::Minutes(5));
  histogram_tester_.ExpectUniqueSample(kLaunchHistogram,
                                       BruschettaResult::kSuccess, 1);
}

// Try to launch, but vm_concierge is not available.
TEST_F(BruschettaLauncherTest, WaitConciergeFails) {
  BruschettaResult result;
  FakeConciergeClient()->set_wait_for_service_to_be_available_response(false);

  launcher_->EnsureRunning(StoreResultThenQuitRunLoop(&result));
  run_loop_.Run();

  ASSERT_EQ(result, BruschettaResult::kConciergeUnavailable);
  histogram_tester_.ExpectUniqueSample(
      kLaunchHistogram, BruschettaResult::kConciergeUnavailable, 1);

  ASSERT_FALSE(BruschettaService::GetForProfile(&profile_)
                   ->GetRunningVmsForTesting()
                   .contains(kTestVmName));
}

// Multiple concurrent launch requests are batched into one request.
TEST_F(BruschettaLauncherTest, MultipleLaunchRequestsAreBatched) {
  std::vector<BruschettaResult> results;
  std::vector<BruschettaResult> expected;
  size_t num_concurrent = 3;
  auto callback = base::BindLambdaForTesting(
      [this, &results, num_concurrent](BruschettaResult result) {
        results.push_back(result);
        if (results.size() == num_concurrent) {
          this->run_loop_.Quit();
        }
      });

  for (size_t n = 0; n < num_concurrent; n++) {
    launcher_->EnsureRunning(callback);
    expected.emplace_back(BruschettaResult::kSuccess);
  }
  run_loop_.Run();

  ASSERT_EQ(FakeConciergeClient()->start_vm_call_count(), 1);
  ASSERT_EQ(results, expected);
}

// Multiple non-overlapping launch requests are not batched into one request.
TEST_F(BruschettaLauncherTest, SeparateLaunchRequestsAreNotBatched) {
  int num_repeats = 2;
  BruschettaResult last_result;
  for (int n = 0; n < num_repeats; n++) {
    launcher_->EnsureRunning(StoreResultThenQuitRunLoop(&last_result));
    // Run until we're idle, rather than all the way until the run loop is
    // killed, so we can still run things next time through the loop.
    this->task_environment_.RunUntilIdle();
    ASSERT_EQ(last_result, BruschettaResult::kSuccess);
  }

  ASSERT_EQ(FakeConciergeClient()->start_vm_call_count(), num_repeats);
}

// We should timeout if launch takes too long.
TEST_F(BruschettaLauncherTest, LaunchTimeout) {
  vm_tools::concierge::VmStoppedSignal signal;
  signal.set_name(kTestVmName);
  FakeConciergeClient()->NotifyVmStopped(
      signal);  // Notify stopped to clear the session tracker.

  BruschettaResult last_result = BruschettaResult::kUnknown;
  launcher_->EnsureRunning(StoreResultThenQuitRunLoop(&last_result));
  // Run until we're idle, rather than all the way until the run loop is
  // killed, so we can still run things next time through the loop.
  this->task_environment_.FastForwardBy(base::Minutes(3));
  ASSERT_EQ(last_result, BruschettaResult::kUnknown);  // No result yet.
  this->task_environment_.FastForwardBy(base::Minutes(2));
  ASSERT_EQ(last_result, BruschettaResult::kTimeout);  // Timed out.
  histogram_tester_.ExpectUniqueSample(kLaunchHistogram,
                                       BruschettaResult::kTimeout, 1);

  // The timeout here happens *after* starting the VM, so we still expect it to
  // be registered as running.
  ASSERT_TRUE(BruschettaService::GetForProfile(&profile_)
                  ->GetRunningVmsForTesting()
                  .contains(kTestVmName));
}

TEST_F(BruschettaLauncherTest, LaunchBlockedByPolicy) {
  BruschettaResult result;

  // Clear the enterprise policy, which implicitly blocks VMs from running.
  profile_.GetPrefs()->ClearPref(prefs::kBruschettaVMConfiguration);

  launcher_->EnsureRunning(StoreResultThenQuitRunLoop(&result));
  run_loop_.Run();

  ASSERT_EQ(result, BruschettaResult::kForbiddenByPolicy);
  histogram_tester_.ExpectUniqueSample(kLaunchHistogram,
                                       BruschettaResult::kForbiddenByPolicy, 1);

  ASSERT_FALSE(BruschettaService::GetForProfile(&profile_)
                   ->GetRunningVmsForTesting()
                   .contains(kTestVmName));
}

TEST_F(BruschettaLauncherTest, VtpmEnabledByPolicy) {
  SetVtpmStatus(true);

  BruschettaResult result;
  vm_tools::concierge::StartVmResponse response;
  response.set_success(true);
  response.set_status(vm_tools::concierge::VmStatus::VM_STATUS_RUNNING);
  FakeConciergeClient()->set_start_vm_response(std::move(response));

  launcher_->EnsureRunning(StoreResultThenQuitRunLoop(&result));
  run_loop_.Run();

  ASSERT_EQ(result, BruschettaResult::kSuccess);
  histogram_tester_.ExpectUniqueSample(kLaunchHistogram,
                                       BruschettaResult::kSuccess, 1);

  const auto& running_vms =
      BruschettaService::GetForProfile(&profile_)->GetRunningVmsForTesting();
  auto it = running_vms.find(kTestVmName);
  ASSERT_NE(it, running_vms.end());
  ASSERT_TRUE(it->second.vtpm_enabled);
}

TEST_F(BruschettaLauncherTest, VtpmDisabledByPolicy) {
  SetVtpmStatus(false);

  BruschettaResult result;
  vm_tools::concierge::StartVmResponse response;
  response.set_success(true);
  response.set_status(vm_tools::concierge::VmStatus::VM_STATUS_RUNNING);
  FakeConciergeClient()->set_start_vm_response(std::move(response));

  launcher_->EnsureRunning(StoreResultThenQuitRunLoop(&result));
  run_loop_.Run();

  ASSERT_EQ(result, BruschettaResult::kSuccess);
  histogram_tester_.ExpectUniqueSample(kLaunchHistogram,
                                       BruschettaResult::kSuccess, 1);

  const auto& running_vms =
      BruschettaService::GetForProfile(&profile_)->GetRunningVmsForTesting();
  auto it = running_vms.find(kTestVmName);
  ASSERT_NE(it, running_vms.end());
  ASSERT_FALSE(it->second.vtpm_enabled);
}

}  // namespace bruschetta