chromium/chrome/browser/ui/views/crostini/crostini_ansible_software_config_view_browsertest.cc

// Copyright 2019 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/crostini/crostini_ansible_software_config_view.h"

#include <string>

#include "base/functional/callback_helpers.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "chrome/browser/ash/crostini/ansible/ansible_management_service.h"
#include "chrome/browser/ash/crostini/ansible/ansible_management_test_helper.h"
#include "chrome/browser/ash/crostini/crostini_pref_names.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/guest_os/guest_id.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/views/crostini/crostini_dialogue_browser_test_util.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/test/browser_test.h"
#include "services/network/test/test_network_connection_tracker.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/devicetype_utils.h"

constexpr char kProgressString[] = "Yesh milord. More work?";

class CrostiniAnsibleSoftwareConfigViewBrowserTest
    : public CrostiniDialogBrowserTest,
      public crostini::AnsibleManagementService::Observer {
 public:
  CrostiniAnsibleSoftwareConfigViewBrowserTest()
      : CrostiniDialogBrowserTest(true /*register_termina*/),
        network_connection_tracker_(
            network::TestNetworkConnectionTracker::CreateInstance()) {
    scoped_feature_list_.InitAndEnableFeature(
        features::kCrostiniAnsibleInfrastructure);
  }

  // CrostiniDialogBrowserTest:
  void ShowUi(const std::string& name) override {
    views::Widget* widget = views::DialogDelegate::CreateDialogWidget(
        std::make_unique<CrostiniAnsibleSoftwareConfigView>(
            browser()->profile(), crostini::DefaultContainerId()),
        nullptr, nullptr);
    ansible_management_service()->AddConfigurationTaskForTesting(
        crostini::DefaultContainerId(), widget);
    widget->Show();
  }

  CrostiniAnsibleSoftwareConfigView* ActiveView(
      const guest_os::GuestId& container_id) {
    if (ansible_management_service()->GetDialogWidgetForTesting(container_id)) {
      return (CrostiniAnsibleSoftwareConfigView*)ansible_management_service()
          ->GetDialogWidgetForTesting(container_id)
          ->widget_delegate();
    } else {
      return nullptr;
    }
  }

  // crostini::AnsibleManagementService::Observer
  void OnAnsibleSoftwareConfigurationStarted(
      const guest_os::GuestId& container_id) override {}

  void OnAnsibleSoftwareConfigurationProgress(
      const guest_os::GuestId& container_id,
      const std::vector<std::string>& status_lines) override {}

  void OnAnsibleSoftwareConfigurationFinished(
      const guest_os::GuestId& container_id,
      bool success) override {}

  void OnAnsibleSoftwareConfigurationUiPrompt(
      const guest_os::GuestId& container_id,
      bool interactive) override {
    if (interactive && accept_) {
      ActiveView(container_id)->Accept();
    } else if (!accept_) {
      run_loop()->Quit();
    }
  }

  void OnApplyAnsiblePlaybook(const guest_os::GuestId& container_id) override {
    if (send_ansible_progress_) {
      EXPECT_NE(nullptr, ActiveView(container_id));
      vm_tools::cicerone::ApplyAnsiblePlaybookProgressSignal signal;
      signal.set_status(
          vm_tools::cicerone::ApplyAnsiblePlaybookProgressSignal::IN_PROGRESS);
      signal.set_vm_name(container_id.vm_name);
      signal.set_container_name(container_id.container_name);
      signal.add_status_string(kProgressString);
      ansible_management_service()->OnApplyAnsiblePlaybookProgress(signal);
      status_string_ =
          ActiveView(container_id)->GetProgressLabelStringForTesting();
    }
    if (is_apply_ansible_success_) {
      EXPECT_NE(nullptr, ActiveView(container_id));
      vm_tools::cicerone::ApplyAnsiblePlaybookProgressSignal signal;
      signal.set_status(
          vm_tools::cicerone::ApplyAnsiblePlaybookProgressSignal::SUCCEEDED);
      signal.set_vm_name(container_id.vm_name);
      signal.set_container_name(container_id.container_name);
      ansible_management_service()->OnApplyAnsiblePlaybookProgress(signal);
    } else {
      EXPECT_NE(nullptr, ActiveView(container_id));

      vm_tools::cicerone::ApplyAnsiblePlaybookProgressSignal signal;
      signal.set_status(
          vm_tools::cicerone::ApplyAnsiblePlaybookProgressSignal::FAILED);
      signal.set_vm_name(container_id.vm_name);
      signal.set_container_name(container_id.container_name);
      signal.set_failure_details("apple");
      ansible_management_service()->OnApplyAnsiblePlaybookProgress(signal);
    }
  }
  void OnAnsibleSoftwareInstall(
      const guest_os::GuestId& container_id) override {
    if (is_install_ansible_success_) {
      EXPECT_NE(nullptr, ActiveView(container_id));
      EXPECT_TRUE(IsDefaultDialog(container_id));
      ansible_management_service()->OnInstallLinuxPackageProgress(
          container_id, crostini::InstallLinuxPackageProgressStatus::SUCCEEDED,
          100,
          /*error_message=*/{});
    } else {
      EXPECT_NE(nullptr, ActiveView(container_id));
      EXPECT_TRUE(IsDefaultDialog(container_id));

      ansible_management_service()->OnInstallLinuxPackageProgress(
          container_id, crostini::InstallLinuxPackageProgressStatus::FAILED, 0,
          /*error_message=*/{});
    }
  }

 protected:
  void SetUpOnMainThread() override {
    // NetworkConnectionTracker should be reset first.
    content::SetNetworkConnectionTrackerForTesting(nullptr);
    content::SetNetworkConnectionTrackerForTesting(
        network_connection_tracker_.get());

    test_helper_ = std::make_unique<crostini::AnsibleManagementTestHelper>(
        browser()->profile());
    test_helper_->SetUpAnsiblePlaybookPreference();
    run_loop_ = std::make_unique<base::RunLoop>();
    ansible_management_service()->AddObserver(this);

    // Set sensible defaults.
    accept_ = true;
    is_install_ansible_success_ = true;
    is_apply_ansible_success_ = true;
    send_ansible_progress_ = false;
  }

  void TearDownOnMainThread() override {
    ansible_management_service()->RemoveObserver(this);
  }

  void SetConnectionType(network::mojom::ConnectionType type) {
    network_connection_tracker_->SetConnectionType(type);
  }

  // A new Widget was created in ShowUi() or since the last VerifyUi().
  bool HasView(const guest_os::GuestId& container_id) {
    return VerifyUi() && ActiveView(container_id);
  }

  // No new Widget was created in ShowUi() or since last VerifyUi().
  bool HasNoView(const guest_os::GuestId& container_id) {
    base::RunLoop().RunUntilIdle();
    return !VerifyUi() && ActiveView(container_id) == nullptr;
  }

  bool IsDefaultDialog(const guest_os::GuestId& container_id) {
    return !HasAcceptButton(container_id) && HasCancelButton(container_id) &&
           HasDefaultStrings(container_id);
  }

  bool IsErrorDialog(const guest_os::GuestId& container_id) {
    return HasAcceptButton(container_id) && !HasCancelButton(container_id) &&
           HasErrorStrings(container_id);
  }

  bool IsErrorOfflineDialog(const guest_os::GuestId& container_id) {
    return HasAcceptButton(container_id) && HasCancelButton(container_id) &&
           HasErrorOfflineStrings(container_id);
  }

  base::RunLoop* run_loop() { return run_loop_.get(); }

  crostini::AnsibleManagementService* ansible_management_service() {
    return crostini::AnsibleManagementService::GetForProfile(
        browser()->profile());
  }

  void SetApplyAnsibleStatus(bool success) {
    is_apply_ansible_success_ = success;
  }

  void SetInstallAnsibleStatus(bool success) {
    is_install_ansible_success_ = success;
  }

  void SetSendAnsibleProgress(bool show_progress) {
    send_ansible_progress_ = show_progress;
  }

  void SetAccept(bool accept) { accept_ = accept; }

  std::u16string status_string_;

 private:
  bool HasAcceptButton(const guest_os::GuestId& container_id) {
    return ActiveView(container_id)->GetOkButton() != nullptr;
  }
  bool HasCancelButton(const guest_os::GuestId& container_id) {
    return ActiveView(container_id)->GetCancelButton() != nullptr;
  }

  bool HasDefaultStrings(const guest_os::GuestId& container_id) {
    return ActiveView(container_id)->GetWindowTitle() ==
               l10n_util::GetStringUTF16(
                   IDS_CROSTINI_ANSIBLE_SOFTWARE_CONFIG_LABEL) +
                   u": " + base::UTF8ToUTF16(container_id.container_name) &&
           ActiveView(container_id)->GetSubtextLabelStringForTesting() ==
               l10n_util::GetStringUTF16(
                   IDS_CROSTINI_ANSIBLE_SOFTWARE_CONFIG_SUBTEXT);
  }

  bool HasErrorStrings(const guest_os::GuestId& container_id) {
    return ActiveView(container_id)->GetWindowTitle() ==
               l10n_util::GetStringUTF16(
                   IDS_CROSTINI_ANSIBLE_SOFTWARE_CONFIG_ERROR_LABEL) +
                   u": " + base::UTF8ToUTF16(container_id.container_name) &&
           ActiveView(container_id)->GetSubtextLabelStringForTesting() ==
               l10n_util::GetStringUTF16(
                   IDS_CROSTINI_ANSIBLE_SOFTWARE_CONFIG_ERROR_SUBTEXT);
  }

  bool HasErrorOfflineStrings(const guest_os::GuestId& container_id) {
    return ActiveView(container_id)->GetWindowTitle() ==
               l10n_util::GetStringFUTF16(
                   IDS_CROSTINI_ANSIBLE_SOFTWARE_CONFIG_ERROR_OFFLINE_LABEL,
                   ui::GetChromeOSDeviceName()) +
                   u": " + base::UTF8ToUTF16(container_id.container_name) &&
           ActiveView(container_id)->GetSubtextLabelStringForTesting() ==
               l10n_util::GetStringUTF16(
                   IDS_CROSTINI_ANSIBLE_SOFTWARE_CONFIG_ERROR_OFFLINE_SUBTEXT);
  }

  bool accept_;
  bool is_install_ansible_success_;
  bool is_apply_ansible_success_;
  bool send_ansible_progress_;
  std::unique_ptr<network::TestNetworkConnectionTracker>
      network_connection_tracker_;
  std::unique_ptr<crostini::AnsibleManagementTestHelper> test_helper_;
  std::unique_ptr<base::RunLoop> run_loop_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

IN_PROC_BROWSER_TEST_F(CrostiniAnsibleSoftwareConfigViewBrowserTest,
                       InvokeUi_default) {
  ShowAndVerifyUi();
}

IN_PROC_BROWSER_TEST_F(CrostiniAnsibleSoftwareConfigViewBrowserTest,
                       SuccessfulFlow) {
  ShowUi("default");

  EXPECT_TRUE(HasView(crostini::DefaultContainerId()));
  EXPECT_TRUE(IsDefaultDialog(crostini::DefaultContainerId()));

  ActiveView(crostini::DefaultContainerId())
      ->OnAnsibleSoftwareConfigurationFinished(crostini::DefaultContainerId(),
                                               true);

  EXPECT_TRUE(HasNoView(crostini::DefaultContainerId()));
}

IN_PROC_BROWSER_TEST_F(CrostiniAnsibleSoftwareConfigViewBrowserTest,
                       SuccessfulFlow_Cancel) {
  ShowUi("default");

  EXPECT_TRUE(HasView(crostini::DefaultContainerId()));
  EXPECT_TRUE(IsDefaultDialog(crostini::DefaultContainerId()));

  ActiveView(crostini::DefaultContainerId())->Cancel();

  EXPECT_TRUE(HasNoView(crostini::DefaultContainerId()));
}

IN_PROC_BROWSER_TEST_F(CrostiniAnsibleSoftwareConfigViewBrowserTest,
                       UnsuccessfulFlow) {
  ShowUi("default");

  EXPECT_TRUE(HasView(crostini::DefaultContainerId()));
  EXPECT_TRUE(IsDefaultDialog(crostini::DefaultContainerId()));

  ActiveView(crostini::DefaultContainerId())
      ->OnAnsibleSoftwareConfigurationFinished(crostini::DefaultContainerId(),
                                               false);

  EXPECT_NE(nullptr, ActiveView(crostini::DefaultContainerId()));
  EXPECT_TRUE(IsErrorDialog(crostini::DefaultContainerId()));
}

IN_PROC_BROWSER_TEST_F(CrostiniAnsibleSoftwareConfigViewBrowserTest,
                       UnsuccessfulFlow_Offline) {
  SetConnectionType(network::mojom::ConnectionType::CONNECTION_NONE);

  ShowUi("default");

  EXPECT_TRUE(HasView(crostini::DefaultContainerId()));
  EXPECT_TRUE(IsDefaultDialog(crostini::DefaultContainerId()));

  ActiveView(crostini::DefaultContainerId())
      ->OnAnsibleSoftwareConfigurationFinished(crostini::DefaultContainerId(),
                                               false);

  EXPECT_NE(nullptr, ActiveView(crostini::DefaultContainerId()));
  EXPECT_TRUE(IsErrorOfflineDialog(crostini::DefaultContainerId()));
}

IN_PROC_BROWSER_TEST_F(CrostiniAnsibleSoftwareConfigViewBrowserTest,
                       UnsuccessfulFlow_Offline_CanRetry) {
  SetConnectionType(network::mojom::ConnectionType::CONNECTION_NONE);

  ShowUi("default");

  EXPECT_TRUE(HasView(crostini::DefaultContainerId()));
  EXPECT_TRUE(IsDefaultDialog(crostini::DefaultContainerId()));

  ActiveView(crostini::DefaultContainerId())
      ->OnAnsibleSoftwareConfigurationFinished(crostini::DefaultContainerId(),
                                               false);

  EXPECT_NE(nullptr, ActiveView(crostini::DefaultContainerId()));
  EXPECT_TRUE(IsErrorOfflineDialog(crostini::DefaultContainerId()));

  // Retry button clicked.
  ActiveView(crostini::DefaultContainerId())->AcceptDialog();

  EXPECT_NE(nullptr, ActiveView(crostini::DefaultContainerId()));
  EXPECT_TRUE(IsDefaultDialog(crostini::DefaultContainerId()));
}

IN_PROC_BROWSER_TEST_F(CrostiniAnsibleSoftwareConfigViewBrowserTest,
                       UnsuccessfulFlow_Offline_Cancel) {
  SetConnectionType(network::mojom::ConnectionType::CONNECTION_NONE);

  ShowUi("default");

  EXPECT_TRUE(HasView(crostini::DefaultContainerId()));
  EXPECT_TRUE(IsDefaultDialog(crostini::DefaultContainerId()));

  ActiveView(crostini::DefaultContainerId())
      ->OnAnsibleSoftwareConfigurationFinished(crostini::DefaultContainerId(),
                                               false);

  EXPECT_NE(nullptr, ActiveView(crostini::DefaultContainerId()));
  EXPECT_TRUE(IsErrorOfflineDialog(crostini::DefaultContainerId()));

  // Cancel button clicked.
  ActiveView(crostini::DefaultContainerId())->CancelDialog();

  EXPECT_TRUE(HasNoView(crostini::DefaultContainerId()));
}

IN_PROC_BROWSER_TEST_F(CrostiniAnsibleSoftwareConfigViewBrowserTest,
                       AnsibleConfigFlow_Successful) {
  ansible_management_service()->ConfigureContainer(
      crostini::DefaultContainerId(),
      browser()->profile()->GetPrefs()->GetFilePath(
          crostini::prefs::kCrostiniAnsiblePlaybookFilePath),
      base::BindLambdaForTesting([&](bool success) { run_loop()->Quit(); }));

  run_loop()->Run();

  EXPECT_TRUE(HasNoView(crostini::DefaultContainerId()));
}

IN_PROC_BROWSER_TEST_F(CrostiniAnsibleSoftwareConfigViewBrowserTest,
                       AnsibleConfigFlow_Multiple_Successful) {
  guest_os::GuestId container1(vm_tools::apps::VmType::TERMINA, "intersetingvm",
                               "ihaveseenbeyond");
  guest_os::GuestId container2(vm_tools::apps::VmType::TERMINA, "supersecretvm",
                               "nothingtoseehereofficer");
  bool done_once = false;
  ansible_management_service()->ConfigureContainer(
      container1,
      browser()->profile()->GetPrefs()->GetFilePath(
          crostini::prefs::kCrostiniAnsiblePlaybookFilePath),
      base::BindLambdaForTesting([&](bool success) {
        if (done_once)
          run_loop()->Quit();
        else
          done_once = true;
      }));

  ansible_management_service()->ConfigureContainer(
      container2,
      browser()->profile()->GetPrefs()->GetFilePath(
          crostini::prefs::kCrostiniAnsiblePlaybookFilePath),
      base::BindLambdaForTesting([&](bool success) {
        if (done_once)
          run_loop()->Quit();
        else
          done_once = true;
      }));
  run_loop()->Run();
  EXPECT_TRUE(HasNoView(container1));
  EXPECT_TRUE(HasNoView(container2));
}

IN_PROC_BROWSER_TEST_F(CrostiniAnsibleSoftwareConfigViewBrowserTest,
                       AnsibleConfigFlowWithProgress_Successful) {
  SetSendAnsibleProgress(true);
  ansible_management_service()->ConfigureContainer(
      crostini::DefaultContainerId(),
      browser()->profile()->GetPrefs()->GetFilePath(
          crostini::prefs::kCrostiniAnsiblePlaybookFilePath),
      base::BindLambdaForTesting([&](bool success) { run_loop()->Quit(); }));

  run_loop()->Run();
  std::u16string expected;
  // -1 to anti the null at the end of the string.
  ASSERT_TRUE(base::UTF8ToUTF16(kProgressString, sizeof(kProgressString) - 1,
                                &expected));
  EXPECT_EQ(status_string_, expected);

  EXPECT_TRUE(HasNoView(crostini::DefaultContainerId()));
}

IN_PROC_BROWSER_TEST_F(CrostiniAnsibleSoftwareConfigViewBrowserTest,
                       AnsibleConfigFlow_InstallationFailed) {
  // Set install failure. No need to set apply because it should never reach
  // there.
  SetAccept(false);
  SetInstallAnsibleStatus(false);
  ansible_management_service()->ConfigureContainer(
      crostini::DefaultContainerId(),
      browser()->profile()->GetPrefs()->GetFilePath(
          crostini::prefs::kCrostiniAnsiblePlaybookFilePath),
      base::BindLambdaForTesting([&](bool success) { run_loop()->Quit(); }));

  run_loop()->Run();

  EXPECT_NE(nullptr, ActiveView(crostini::DefaultContainerId()));
  EXPECT_TRUE(IsErrorDialog(crostini::DefaultContainerId()));
}

IN_PROC_BROWSER_TEST_F(CrostiniAnsibleSoftwareConfigViewBrowserTest,
                       AnsibleConfigFlow_ApplicationFailed) {
  // Set apply failure
  SetAccept(false);
  SetApplyAnsibleStatus(false);
  ansible_management_service()->ConfigureContainer(
      crostini::DefaultContainerId(),
      browser()->profile()->GetPrefs()->GetFilePath(
          crostini::prefs::kCrostiniAnsiblePlaybookFilePath),
      base::BindLambdaForTesting(
          [&](bool success) { std::move(run_loop()->QuitClosure()).Run(); }));

  run_loop()->Run();

  EXPECT_NE(nullptr, ActiveView(crostini::DefaultContainerId()));
  EXPECT_TRUE(IsErrorDialog(crostini::DefaultContainerId()));
}