chromium/chrome/browser/ui/views/plugin_vm/plugin_vm_installer_view_browsertest.cc

// Copyright 2018 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/ui/views/plugin_vm/plugin_vm_installer_view.h"

#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_installer_factory.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_pref_names.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_test_helper.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_util.h"
#include "chrome/browser/ash/settings/scoped_testing_cros_settings.h"
#include "chrome/browser/ash/settings/stub_cros_settings_provider.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/test/test_browser_dialog.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/dbus/concierge/fake_concierge_client.h"
#include "chromeos/ash/components/dbus/debug_daemon/fake_debug_daemon_client.h"
#include "chromeos/ash/components/dbus/vm_plugin_dispatcher/fake_vm_plugin_dispatcher_client.h"
#include "chromeos/ash/components/install_attributes/stub_install_attributes.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "components/download/public/background_service/download_metadata.h"
#include "components/download/public/background_service/features.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_utils.h"
#include "services/network/test/test_network_connection_tracker.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/test/ax_event_counter.h"

namespace {

const char kZipFile[] = "/downloads/a_zip_file.zip";
const char kZipFileHash[] =
    "bb077522e6c6fec07cf863ca44d5701935c4bc36ed12ef154f4cc22df70aec18";
const char kNonMatchingHash[] =
    "842841a4c75a55ad050d686f4ea5f77e83ae059877fe9b6946aa63d3d057ed32";
const char kJpgFile[] = "/downloads/image.jpg";
const char kJpgFileHash[] =
    "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b";

}  // namespace

// TODO(timloh): This file should only be responsible for testing the
// interactions between the installer UI and the installer backend. We should
// mock out the backend and move the tests for the backend logic out of here.

class PluginVmInstallerViewBrowserTest : public DialogBrowserTest {
 public:
  PluginVmInstallerViewBrowserTest() = default;

  PluginVmInstallerViewBrowserTest(const PluginVmInstallerViewBrowserTest&) =
      delete;
  PluginVmInstallerViewBrowserTest& operator=(
      const PluginVmInstallerViewBrowserTest&) = delete;

  void SetUpOnMainThread() override {
    ASSERT_TRUE(embedded_test_server()->Start());
    fake_concierge_client_ = ash::FakeConciergeClient::Get();
    fake_concierge_client_->set_disk_image_progress_signal_connected(true);
    fake_vm_plugin_dispatcher_client_ =
        static_cast<ash::FakeVmPluginDispatcherClient*>(
            ash::VmPluginDispatcherClient::Get());

    network_connection_tracker_ =
        network::TestNetworkConnectionTracker::CreateInstance();
    content::SetNetworkConnectionTrackerForTesting(nullptr);
    content::SetNetworkConnectionTrackerForTesting(
        network_connection_tracker_.get());
    network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
        network::mojom::ConnectionType::CONNECTION_WIFI);
  }

  // DialogBrowserTest:
  void ShowUi(const std::string& name) override {
    plugin_vm::ShowPluginVmInstallerView(browser()->profile());
    view_ = PluginVmInstallerView::GetActiveViewForTesting();
  }

 protected:
  bool HasAcceptButton() { return view_->GetOkButton() != nullptr; }

  bool HasCancelButton() { return view_->GetCancelButton() != nullptr; }

  void AllowPluginVm() {
    EnterpriseEnrollDevice();
    SetPluginVmPolicies();
    // Set correct PluginVmImage preference value.
    SetPluginVmImagePref(embedded_test_server()->GetURL(kZipFile).spec(),
                         kZipFileHash);
    auto* installer = plugin_vm::PluginVmInstallerFactory::GetForProfile(
        browser()->profile());
    installer->SetFreeDiskSpaceForTesting(installer->RequiredFreeDiskSpace());
    installer->SkipLicenseCheckForTesting();
  }

  void SetPluginVmImagePref(std::string url, std::string hash) {
    ScopedDictPrefUpdate update(browser()->profile()->GetPrefs(),
                                plugin_vm::prefs::kPluginVmImage);
    base::Value::Dict& plugin_vm_image = update.Get();
    plugin_vm_image.Set("url", url);
    plugin_vm_image.Set("hash", hash);
  }

  void WaitForSetupToFinish() {
    base::RunLoop run_loop;
    view_->SetFinishedCallbackForTesting(
        base::BindOnce(&PluginVmInstallerViewBrowserTest::OnSetupFinished,
                       run_loop.QuitClosure()));

    run_loop.Run();
    content::RunAllTasksUntilIdle();
  }

  void CheckSetupFailed() {
    EXPECT_TRUE(HasAcceptButton());
    EXPECT_TRUE(HasCancelButton());
    EXPECT_EQ(view_->GetDialogButtonLabel(ui::mojom::DialogButton::kOk),
              l10n_util::GetStringUTF16(IDS_PLUGIN_VM_INSTALLER_RETRY_BUTTON));
    EXPECT_EQ(view_->GetTitle(),
              l10n_util::GetStringUTF16(IDS_PLUGIN_VM_INSTALLER_ERROR_TITLE));
  }

  void CheckSetupIsFinishedSuccessfully() {
    EXPECT_TRUE(HasAcceptButton());
    EXPECT_TRUE(HasCancelButton());
    EXPECT_EQ(view_->GetDialogButtonLabel(ui::mojom::DialogButton::kCancel),
              l10n_util::GetStringUTF16(IDS_APP_CLOSE));
    EXPECT_EQ(view_->GetDialogButtonLabel(ui::mojom::DialogButton::kOk),
              l10n_util::GetStringUTF16(IDS_PLUGIN_VM_INSTALLER_LAUNCH_BUTTON));
    EXPECT_EQ(view_->GetTitle(), l10n_util::GetStringUTF16(
                                     IDS_PLUGIN_VM_INSTALLER_FINISHED_TITLE));
  }

  ash::ScopedTestingCrosSettings scoped_testing_cros_settings_;
  ash::ScopedStubInstallAttributes scoped_stub_install_attributes_;

  std::unique_ptr<network::TestNetworkConnectionTracker>
      network_connection_tracker_;
  raw_ptr<PluginVmInstallerView, DanglingUntriaged> view_;
  raw_ptr<ash::FakeConciergeClient, DanglingUntriaged> fake_concierge_client_;
  raw_ptr<ash::FakeVmPluginDispatcherClient, DanglingUntriaged>
      fake_vm_plugin_dispatcher_client_;

 private:
  void EnterpriseEnrollDevice() {
    scoped_stub_install_attributes_.Get()->SetCloudManaged("example.com",
                                                           "device_id");
  }

  void SetPluginVmPolicies() {
    // User polcies.
    browser()->profile()->GetPrefs()->SetBoolean(
        plugin_vm::prefs::kPluginVmAllowed, true);
    // Device policies.
    scoped_testing_cros_settings_.device_settings()->Set(ash::kPluginVmAllowed,
                                                         base::Value(true));
  }

  static void OnSetupFinished(base::OnceClosure quit_closure, bool success) {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(quit_closure));
  }
};

class PluginVmInstallerViewBrowserTestWithFeatureEnabled
    : public PluginVmInstallerViewBrowserTest {
 public:
  PluginVmInstallerViewBrowserTestWithFeatureEnabled() {
    feature_list_.InitWithFeaturesAndParameters(
        {{features::kPluginVm, {}},
         {download::kDownloadServiceFeature, {{"start_up_delay_ms", "0"}}}},
        {});
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

// Test the dialog is actually can be launched.
IN_PROC_BROWSER_TEST_F(PluginVmInstallerViewBrowserTest, InvokeUi_default) {
  ShowAndVerifyUi();
}

IN_PROC_BROWSER_TEST_F(PluginVmInstallerViewBrowserTestWithFeatureEnabled,
                       SetupShouldFinishSuccessfully) {
  AllowPluginVm();
  plugin_vm::SetupConciergeForSuccessfulDiskImageImport(fake_concierge_client_);

  ShowUi("default");
  EXPECT_NE(nullptr, view_);

  view_->AcceptDialog();
  WaitForSetupToFinish();

  CheckSetupIsFinishedSuccessfully();
}

IN_PROC_BROWSER_TEST_F(PluginVmInstallerViewBrowserTestWithFeatureEnabled,
                       SetupShouldFireAccessibilityEvents) {
  views::test::AXEventCounter counter(views::AXEventManager::Get());

  AllowPluginVm();
  plugin_vm::SetupConciergeForSuccessfulDiskImageImport(fake_concierge_client_);
  ShowUi("default");
  EXPECT_NE(nullptr, view_);

  auto* title_view = view_->GetTitleViewForTesting();
  EXPECT_NE(nullptr, title_view);

  auto* message_view = view_->GetMessageViewForTesting();
  EXPECT_NE(nullptr, message_view);

  auto* progress_view = view_->GetDownloadProgressMessageViewForTesting();
  EXPECT_NE(nullptr, progress_view);

  // Views should only fire property-change events when the property changes;
  // not when a value is initialized. As a result, there should not be any
  // text-changed accessibility fired as a result of the introductory/set-up
  // text being displayed.
  EXPECT_EQ(0, counter.GetCount(ax::mojom::Event::kTextChanged, title_view));
  EXPECT_EQ(0, counter.GetCount(ax::mojom::Event::kTextChanged, message_view));
  EXPECT_EQ(0, counter.GetCount(ax::mojom::Event::kTextChanged, progress_view));

  counter.ResetAllCounts();
  view_->AcceptDialog();

  // Once the installation has been accepted, the message and title labels are
  // changed to indicate the installation has begun. Each label should have
  // fired an accessibility event for this change. Because the download has not
  // started, there should be no event from the download progress label.
  EXPECT_EQ(1, counter.GetCount(ax::mojom::Event::kTextChanged, title_view));
  EXPECT_EQ(1, counter.GetCount(ax::mojom::Event::kTextChanged, message_view));
  EXPECT_EQ(0, counter.GetCount(ax::mojom::Event::kTextChanged, progress_view));

  counter.ResetAllCounts();
  WaitForSetupToFinish();

  // During the installation process, the title remains the same until the
  // installation is complete. There should be an accessibility event for the
  // title changing to the setup-complete text.
  EXPECT_EQ(1, counter.GetCount(ax::mojom::Event::kTextChanged, title_view));

  // During the installation process, the message changes three times:
  // downloading, configuring, ready to use. Each time there should be an
  // accessibility event for the change.
  EXPECT_EQ(3, counter.GetCount(ax::mojom::Event::kTextChanged, message_view));

  // During the download process, there are periodic updates showing the
  // amount downloaded thus far. There are six such updates in this test:
  // the first "0 GB" and the next five "0.0 GB". Each time the text changes,
  // there should be an accessibility event for the change. Since the text
  // changed twice, there should be two updates.
  EXPECT_EQ(2, counter.GetCount(ax::mojom::Event::kTextChanged, progress_view));

  CheckSetupIsFinishedSuccessfully();
}

IN_PROC_BROWSER_TEST_F(PluginVmInstallerViewBrowserTestWithFeatureEnabled,
                       SetupShouldFailAsHashesDoNotMatch) {
  AllowPluginVm();
  // Reset PluginVmImage hash to non-matching.
  SetPluginVmImagePref(embedded_test_server()->GetURL(kZipFile).spec(),
                       kNonMatchingHash);

  ShowUi("default");
  EXPECT_NE(nullptr, view_);

  view_->AcceptDialog();
  WaitForSetupToFinish();

  CheckSetupFailed();
}

IN_PROC_BROWSER_TEST_F(PluginVmInstallerViewBrowserTestWithFeatureEnabled,
                       SetupShouldFailAsImportingFails) {
  AllowPluginVm();
  SetPluginVmImagePref(embedded_test_server()->GetURL(kJpgFile).spec(),
                       kJpgFileHash);

  ShowUi("default");
  EXPECT_NE(nullptr, view_);

  view_->AcceptDialog();
  WaitForSetupToFinish();

  CheckSetupFailed();
}

IN_PROC_BROWSER_TEST_F(PluginVmInstallerViewBrowserTestWithFeatureEnabled,
                       CouldRetryAfterFailedSetup) {
  AllowPluginVm();
  // Reset PluginVmImage hash to non-matching.
  SetPluginVmImagePref(embedded_test_server()->GetURL(kZipFile).spec(),
                       kNonMatchingHash);

  ShowUi("default");
  EXPECT_NE(nullptr, view_);

  view_->AcceptDialog();
  WaitForSetupToFinish();

  CheckSetupFailed();

  plugin_vm::SetupConciergeForSuccessfulDiskImageImport(fake_concierge_client_);
  SetPluginVmImagePref(embedded_test_server()->GetURL(kZipFile).spec(),
                       kZipFileHash);

  // Retry button clicked to retry the download.
  view_->AcceptDialog();

  WaitForSetupToFinish();

  CheckSetupIsFinishedSuccessfully();
}

IN_PROC_BROWSER_TEST_F(
    PluginVmInstallerViewBrowserTest,
    SetupShouldShowDisallowedMessageIfPluginVmIsNotAllowedToRun) {
  ShowUi("default");
  EXPECT_NE(nullptr, view_);

  view_->AcceptDialog();

  std::u16string app_name = l10n_util::GetStringUTF16(IDS_PLUGIN_VM_APP_NAME);
  EXPECT_FALSE(HasAcceptButton());
  EXPECT_TRUE(HasCancelButton());
  EXPECT_EQ(view_->GetTitle(),
            l10n_util::GetStringFUTF16(
                IDS_PLUGIN_VM_INSTALLER_NOT_ALLOWED_TITLE, app_name));
  EXPECT_EQ(
      view_->GetMessage(),
      l10n_util::GetStringFUTF16(
          IDS_PLUGIN_VM_INSTALLER_NOT_ALLOWED_MESSAGE, app_name,
          base::NumberToString16(
              static_cast<std::underlying_type_t<
                  plugin_vm::PluginVmInstaller::FailureReason>>(
                  plugin_vm::PluginVmInstaller::FailureReason::NOT_ALLOWED))));
}

IN_PROC_BROWSER_TEST_F(PluginVmInstallerViewBrowserTestWithFeatureEnabled,
                       SetupShouldLaunchIfImageAlreadyImported) {
  AllowPluginVm();

  // Setup concierge and the dispatcher for VM already imported.
  vm_tools::concierge::ListVmDisksResponse list_vm_disks_response;
  list_vm_disks_response.set_success(true);
  auto* image = list_vm_disks_response.add_images();
  image->set_name(plugin_vm::kPluginVmName);
  image->set_storage_location(vm_tools::concierge::STORAGE_CRYPTOHOME_PLUGINVM);
  fake_concierge_client_->set_list_vm_disks_response(list_vm_disks_response);

  vm_tools::plugin_dispatcher::ListVmResponse list_vms_response;
  list_vms_response.add_vm_info()->set_state(
      vm_tools::plugin_dispatcher::VmState::VM_STATE_STOPPED);
  fake_vm_plugin_dispatcher_client_->set_list_vms_response(list_vms_response);

  fake_vm_plugin_dispatcher_client_->set_start_vm_response(
      vm_tools::plugin_dispatcher::StartVmResponse());

  ShowUi("default");
  EXPECT_NE(nullptr, view_);

  view_->AcceptDialog();
  WaitForSetupToFinish();

  // Installer should be closed.
  EXPECT_EQ(nullptr, PluginVmInstallerView::GetActiveViewForTesting());
}