chromium/chrome/browser/ash/borealis/borealis_installer_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/ash/borealis/borealis_installer.h"

#include <memory>
#include <ratio>
#include <string_view>

#include "base/functional/callback_helpers.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/time/time.h"
#include "chrome/browser/ash/borealis/borealis_context_manager.h"
#include "chrome/browser/ash/borealis/borealis_features.h"
#include "chrome/browser/ash/borealis/borealis_metrics.h"
#include "chrome/browser/ash/borealis/borealis_prefs.h"
#include "chrome/browser/ash/borealis/borealis_service.h"
#include "chrome/browser/ash/borealis/borealis_types.mojom.h"
#include "chrome/browser/ash/borealis/borealis_util.h"
#include "chrome/browser/ash/borealis/testing/apps.h"
#include "chrome/browser/ash/borealis/testing/callback_factory.h"
#include "chrome/browser/ash/borealis/testing/features.h"
#include "chrome/browser/ash/guest_os/dbus_test_helper.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.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/fake_dlcservice_client.h"
#include "chromeos/ash/components/dbus/spaced/fake_spaced_client.h"
#include "chromeos/ash/components/dbus/spaced/spaced_client.h"
#include "chromeos/ash/components/dbus/vm_applications/apps.pb.h"
#include "chromeos/ash/components/dbus/vm_concierge/concierge_service.pb.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_task_environment.h"
#include "services/network/test/test_network_connection_tracker.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/cros_system_api/dbus/dlcservice/dbus-constants.h"

namespace borealis {

namespace {

using ::testing::_;
using ::testing::Mock;
using ::testing::NiceMock;
using InstallingState = BorealisInstaller::InstallingState;
using borealis::mojom::InstallResult;

class MockObserver : public BorealisInstaller::Observer {
 public:
  MOCK_METHOD1(OnProgressUpdated, void(double));
  MOCK_METHOD1(OnStateUpdated, void(InstallingState));
  MOCK_METHOD2(OnInstallationEnded, void(InstallResult, const std::string&));
  MOCK_METHOD0(OnCancelInitiated, void());
};

class BorealisInstallerTest : public testing::Test,
                              protected guest_os::FakeVmServicesHelper {
 public:
  BorealisInstallerTest()
      : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  ~BorealisInstallerTest() override = default;

  // Disallow copy and assign.
  BorealisInstallerTest(const BorealisInstallerTest&) = delete;
  BorealisInstallerTest& operator=(const BorealisInstallerTest&) = delete;

 protected:
  BorealisInstaller* installer() {
    return &BorealisService::GetForProfile(&profile_)->Installer();
  }

  void SetUp() override {
    // TODO(b/293370103): Remove this when we remove the legacy disk management.
    if (!ash::SpacedClient::Get()) {
      ash::SpacedClient::InitializeFake();
      static_cast<ash::FakeSpacedClient*>(ash::SpacedClient::Get())
          ->set_free_disk_space(100 * std::giga::num);
    }

    scoped_allowance_ =
        std::make_unique<ScopedAllowBorealis>(&profile_, /*also_enable=*/false);

    FakeDlcserviceClient()->set_install_error(dlcservice::kErrorNone);
    guest_os::GuestId id{guest_os::VmType::BOREALIS, "borealis", "penguin"};
    guest_os::GuestOsSessionTracker::GetForProfile(&profile_)
        ->AddGuestForTesting(id);

    vm_tools::concierge::ListVmDisksResponse resp;
    vm_tools::concierge::VmDiskInfo* img = resp.add_images();
    img->set_name("borealis");
    img->set_user_chosen_size(false);
    resp.set_success(true);
    FakeConciergeClient()->set_list_vm_disks_response(resp);

    // Adding the steam app this early is somewhat unrealistic, but sufficient
    // for testing.
    //
    // A better place would be some time after the StartVm() rpc returns.
    CreateFakeMainApp(&profile_);

    ASSERT_FALSE(BorealisDlcInstalled());
    ASSERT_FALSE(
        BorealisService::GetForProfile(&profile_)->Features().IsEnabled());
  }

  void StartAndRunToCompletion() {
    installer()->Start();
    task_environment_.RunUntilIdle();
  }

  bool BorealisDlcInstalled() {
    base::RunLoop run_loop;
    bool installed = false;
    FakeDlcserviceClient()->GetExistingDlcs(base::BindLambdaForTesting(
        [&](std::string_view err,
            const dlcservice::DlcsWithContent& dlcs_with_content) {
          for (const auto& dlc : dlcs_with_content.dlc_infos()) {
            if (dlc.id() == kBorealisDlcName) {
              installed = true;
              break;
            }
          }
          run_loop.Quit();
        }));
    run_loop.Run();
    return installed;
  }

  content::BrowserTaskEnvironment task_environment_;
  base::HistogramTester histogram_tester_;
  TestingProfile profile_;
  std::unique_ptr<ScopedAllowBorealis> scoped_allowance_;
};

class BorealisInstallerTestDlc : public BorealisInstallerTest,
                                 public testing::WithParamInterface<
                                     std::pair<std::string, InstallResult>> {};

TEST_F(BorealisInstallerTest, BorealisNotAllowed) {
  scoped_allowance_.reset();

  StartAndRunToCompletion();

  EXPECT_FALSE(BorealisDlcInstalled());
  EXPECT_FALSE(
      BorealisService::GetForProfile(&profile_)->Features().IsEnabled());
}

TEST_F(BorealisInstallerTest, DeviceOfflineInstallationFails) {
  std::unique_ptr<network::TestNetworkConnectionTracker>
      network_connection_tracker =
          network::TestNetworkConnectionTracker::CreateInstance();
  network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
      network::mojom::ConnectionType::CONNECTION_NONE);

  StartAndRunToCompletion();

  EXPECT_FALSE(BorealisDlcInstalled());
  EXPECT_FALSE(
      BorealisService::GetForProfile(&profile_)->Features().IsEnabled());
}

TEST_F(BorealisInstallerTest, SucessfulInstallation) {
  StartAndRunToCompletion();

  EXPECT_TRUE(BorealisDlcInstalled());
  EXPECT_TRUE(
      BorealisService::GetForProfile(&profile_)->Features().IsEnabled());
}

TEST_F(BorealisInstallerTest, InstallationObserver) {
  testing::StrictMock<MockObserver> observer;
  installer()->AddObserver(&observer);

  EXPECT_CALL(observer, OnStateUpdated(InstallingState::kCheckingIfAllowed));
  EXPECT_CALL(observer, OnStateUpdated(InstallingState::kInstallingDlc));
  EXPECT_CALL(observer, OnStateUpdated(InstallingState::kStartingUp));
  EXPECT_CALL(observer, OnStateUpdated(InstallingState::kAwaitingApplications));
  EXPECT_CALL(observer, OnProgressUpdated(_)).Times(testing::AtLeast(1));
  EXPECT_CALL(observer, OnInstallationEnded(InstallResult::kSuccess, ""));

  StartAndRunToCompletion();
}

TEST_F(BorealisInstallerTest, CancelledInstallation) {
  testing::NiceMock<MockObserver> observer;
  installer()->AddObserver(&observer);
  FakeDlcserviceClient()->set_install_error(dlcservice::kErrorNone);

  EXPECT_CALL(observer, OnCancelInitiated());
  EXPECT_CALL(observer,
              OnInstallationEnded(InstallResult::kCancelled, testing::Not("")));

  installer()->Start();
  installer()->Cancel();
  task_environment_.RunUntilIdle();
}

TEST_F(BorealisInstallerTest, InstallationInProgess) {
  testing::NiceMock<MockObserver> observer;
  installer()->AddObserver(&observer);

  EXPECT_CALL(observer,
              OnInstallationEnded(InstallResult::kBorealisInstallInProgress,
                                  testing::Not("")));
  EXPECT_CALL(observer, OnInstallationEnded(InstallResult::kSuccess, ""));

  installer()->Start();
  installer()->Start();
  task_environment_.RunUntilIdle();
}

TEST_F(BorealisInstallerTest, CancelledThenSuccessfulInstallation) {
  installer()->Cancel();
  task_environment_.RunUntilIdle();

  EXPECT_FALSE(BorealisDlcInstalled());
  EXPECT_FALSE(
      BorealisService::GetForProfile(&profile_)->Features().IsEnabled());

  installer()->Start();
  task_environment_.RunUntilIdle();

  EXPECT_TRUE(BorealisDlcInstalled());
  EXPECT_TRUE(
      BorealisService::GetForProfile(&profile_)->Features().IsEnabled());
}

TEST_F(BorealisInstallerTest, SucessfulInstallationRecordMetrics) {
  StartAndRunToCompletion();

  histogram_tester_.ExpectTotalCount(kBorealisInstallNumAttemptsHistogram, 1);
  histogram_tester_.ExpectUniqueSample(kBorealisInstallResultHistogram,
                                       InstallResult::kSuccess, 1);
  histogram_tester_.ExpectTotalCount(kBorealisInstallOverallTimeHistogram, 1);
}

TEST_F(BorealisInstallerTest, IncompleteInstallationRecordMetrics) {
  // This error is arbitrarily chosen for simplicity.
  FakeDlcserviceClient()->set_install_error(dlcservice::kErrorAllocation);

  StartAndRunToCompletion();

  histogram_tester_.ExpectTotalCount(kBorealisInstallNumAttemptsHistogram, 1);
  histogram_tester_.ExpectUniqueSample(kBorealisInstallResultHistogram,
                                       InstallResult::kDlcNeedSpaceError, 1);
  histogram_tester_.ExpectTotalCount(kBorealisInstallOverallTimeHistogram, 0);
}

TEST_F(BorealisInstallerTest, ReportsStartupFailureAsError) {
  vm_tools::concierge::StartVmResponse resp;
  resp.set_success(false);
  resp.set_failure_reason("ABC123");
  FakeConciergeClient()->set_start_vm_response(resp);

  testing::NiceMock<MockObserver> observer;
  installer()->AddObserver(&observer);
  EXPECT_CALL(observer, OnInstallationEnded(InstallResult::kStartupFailed,
                                            testing::HasSubstr("ABC123")));

  StartAndRunToCompletion();
}

TEST_F(BorealisInstallerTest, ReportsMainAppMissingAsError) {
  // Remove the steam client app, which the framework made for us
  guest_os::GuestOsRegistryServiceFactory::GetForProfile(&profile_)
      ->ClearApplicationList(guest_os::VmType::BOREALIS, "borealis", "penguin");

  testing::NiceMock<MockObserver> observer;
  installer()->AddObserver(&observer);

  StartAndRunToCompletion();

  EXPECT_CALL(observer, OnInstallationEnded(InstallResult::kMainAppNotPresent,
                                            testing::Not("")));
  task_environment_.FastForwardBy(base::Seconds(6));
}

// Note that we don't check if the DLC has/hasn't been installed, since the
// mocked DLC service will always succeed, so we only care about how the error
// code returned by the service is handled by the installer.
TEST_P(BorealisInstallerTestDlc, DlcError) {
  testing::NiceMock<MockObserver> observer;
  installer()->AddObserver(&observer);
  FakeDlcserviceClient()->set_install_error(GetParam().first);

  EXPECT_CALL(observer,
              OnInstallationEnded(GetParam().second, testing::Not("")));

  StartAndRunToCompletion();
}

INSTANTIATE_TEST_SUITE_P(
    BorealisInstallerTestDlcErrors,
    BorealisInstallerTestDlc,
    testing::Values(std::pair<std::string, InstallResult>(
                        dlcservice::kErrorInvalidDlc,
                        InstallResult::kDlcUnsupportedError),
                    std::pair<std::string, InstallResult>(
                        dlcservice::kErrorNeedReboot,
                        InstallResult::kDlcNeedRebootError),
                    std::pair<std::string, InstallResult>(
                        dlcservice::kErrorAllocation,
                        InstallResult::kDlcNeedSpaceError),
                    std::pair<std::string, InstallResult>(
                        dlcservice::kErrorNoImageFound,
                        InstallResult::kDlcNeedUpdateError),
                    std::pair<std::string, InstallResult>(
                        "unknown",
                        InstallResult::kDlcUnknownError)));

class BorealisUninstallerTest : public BorealisInstallerTest {
 public:
  void SetUp() override {
    BorealisInstallerTest::SetUp();

    // Install borealis.
    StartAndRunToCompletion();
    ASSERT_TRUE(
        BorealisService::GetForProfile(&profile_)->Features().IsEnabled());
  }
};

using CallbackFactory = StrictCallbackFactory<void(BorealisUninstallResult)>;

TEST_F(BorealisUninstallerTest, ErrorIfUninstallIsAlreadyInProgress) {
  CallbackFactory callback_factory;

  EXPECT_CALL(callback_factory,
              Call(BorealisUninstallResult::kAlreadyInProgress))
      .Times(1);

  installer()->Uninstall(callback_factory.BindOnce());
  installer()->Uninstall(callback_factory.BindOnce());
}

TEST_F(BorealisUninstallerTest, ErrorIfShutdownFails) {
  CallbackFactory callback_factory;
  EXPECT_CALL(callback_factory, Call(BorealisUninstallResult::kShutdownFailed));

  FakeConciergeClient()->set_stop_vm_response(std::nullopt);

  installer()->Uninstall(callback_factory.BindOnce());
  task_environment_.RunUntilIdle();

  // Shutdown failed, so borealis's disk will still be there.
  EXPECT_EQ(FakeConciergeClient()->destroy_disk_image_call_count(), 0);

  // Borealis is still "installed" according to the prefs.
  EXPECT_TRUE(
      profile_.GetPrefs()->GetBoolean(prefs::kBorealisInstalledOnDevice));
}

TEST_F(BorealisUninstallerTest, ErrorIfDiskNotRemoved) {
  CallbackFactory callback_factory;
  EXPECT_CALL(callback_factory,
              Call(BorealisUninstallResult::kRemoveDiskFailed));

  FakeConciergeClient()->set_destroy_disk_image_response(std::nullopt);

  installer()->Uninstall(callback_factory.BindOnce());
  task_environment_.RunUntilIdle();

  // The DLC should remain because the disk was not removed.
  EXPECT_TRUE(BorealisDlcInstalled());

  // Borealis is still "installed" according to the prefs.
  EXPECT_TRUE(
      profile_.GetPrefs()->GetBoolean(prefs::kBorealisInstalledOnDevice));
}

TEST_F(BorealisUninstallerTest, ErrorIfDlcNotRemoved) {
  CallbackFactory callback_factory;
  EXPECT_CALL(callback_factory,
              Call(BorealisUninstallResult::kRemoveDlcFailed));

  FakeDlcserviceClient()->set_uninstall_error("some failure");

  installer()->Uninstall(callback_factory.BindOnce());
  task_environment_.RunUntilIdle();

  // Borealis is still "installed" according to the prefs.
  EXPECT_TRUE(
      profile_.GetPrefs()->GetBoolean(prefs::kBorealisInstalledOnDevice));
}

TEST_F(BorealisUninstallerTest, UninstallationRemovesAllNecessaryPieces) {
  CallbackFactory callback_factory;
  EXPECT_CALL(callback_factory, Call(BorealisUninstallResult::kSuccess));

  // Install a fake app.
  CreateFakeApp(&profile_, "test.desktop", "test exec");
  task_environment_.RunUntilIdle();
  EXPECT_EQ(guest_os::GuestOsRegistryServiceFactory::GetForProfile(&profile_)
                ->GetRegisteredApps(vm_tools::apps::BOREALIS)
                .size(),
            1u);

  installer()->Uninstall(callback_factory.BindOnce());
  task_environment_.RunUntilIdle();

  // Borealis is not running.
  EXPECT_FALSE(
      BorealisService::GetForProfile(&profile_)->ContextManager().IsRunning());

  // Borealis is not enabled.
  EXPECT_FALSE(
      BorealisService::GetForProfile(&profile_)->Features().IsEnabled());

  // Borealis has no installed apps.
  EXPECT_EQ(guest_os::GuestOsRegistryServiceFactory::GetForProfile(&profile_)
                ->GetRegisteredApps(vm_tools::apps::BOREALIS)
                .size(),
            0u);

  // Borealis has no stateful disk.
  EXPECT_GE(FakeConciergeClient()->destroy_disk_image_call_count(), 1);

  // Borealis's DLC is not installed
  EXPECT_FALSE(BorealisDlcInstalled());
}

TEST_F(BorealisUninstallerTest, UninstallationIsIdempotent) {
  CallbackFactory callback_factory;
  EXPECT_CALL(callback_factory, Call(BorealisUninstallResult::kSuccess))
      .Times(2);

  installer()->Uninstall(callback_factory.BindOnce());
  task_environment_.RunUntilIdle();

  installer()->Uninstall(callback_factory.BindOnce());
  task_environment_.RunUntilIdle();
}

TEST_F(BorealisUninstallerTest, SuccessfulUninstallationRecordsMetrics) {
  installer()->Uninstall(base::DoNothing());
  task_environment_.RunUntilIdle();

  histogram_tester_.ExpectTotalCount(kBorealisUninstallNumAttemptsHistogram, 1);
  histogram_tester_.ExpectUniqueSample(kBorealisUninstallResultHistogram,
                                       BorealisUninstallResult::kSuccess, 1);
}

TEST_F(BorealisUninstallerTest, FailedUninstallationRecordsMetrics) {
  // Fail via shutdown, as that is the first step.
  FakeConciergeClient()->set_stop_vm_response(std::nullopt);

  installer()->Uninstall(base::DoNothing());
  task_environment_.RunUntilIdle();

  histogram_tester_.ExpectTotalCount(kBorealisUninstallNumAttemptsHistogram, 1);
  histogram_tester_.ExpectUniqueSample(kBorealisUninstallResultHistogram,
                                       BorealisUninstallResult::kShutdownFailed,
                                       1);
}

}  // namespace
}  // namespace borealis