chromium/chrome/test/chromeos/standalone_browser_test_controller.cc

// Copyright 2021 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/test/chromeos/standalone_browser_test_controller.h"

#include <memory>

#include "base/check_is_test.h"
#include "base/functional/bind.h"
#include "base/functional/overloaded.h"
#include "base/json/json_reader.h"
#include "base/memory/raw_ptr.h"
#include "base/test/values_test_util.h"
#include "base/types/expected_macros.h"
#include "base/values.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/component_loader.h"
#include "chrome/browser/extensions/extension_keeplist_chromeos.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/speech/tts_crosapi_util.h"
#include "chrome/browser/ui/webui/print_preview/extension_printer_service_provider_lacros.h"
#include "chrome/browser/ui/webui/print_preview/printer_handler.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_source.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_source.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_trust_checker.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "chromeos/crosapi/mojom/tts.mojom-forward.h"
#include "components/prefs/pref_service.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "content/public/browser/tts_utterance.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_constants.h"
#include "url/origin.h"

namespace {

blink::mojom::DisplayMode WindowModeToDisplayMode(
    apps::WindowMode window_mode) {
  switch (window_mode) {
    case apps::WindowMode::kBrowser:
      return blink::mojom::DisplayMode::kBrowser;
    case apps::WindowMode::kTabbedWindow:
      return blink::mojom::DisplayMode::kTabbed;
    case apps::WindowMode::kWindow:
      return blink::mojom::DisplayMode::kStandalone;
    case apps::WindowMode::kUnknown:
      return blink::mojom::DisplayMode::kUndefined;
  }
}

web_app::mojom::UserDisplayMode WindowModeToUserDisplayMode(
    apps::WindowMode window_mode) {
  switch (window_mode) {
    case apps::WindowMode::kBrowser:
      return web_app::mojom::UserDisplayMode::kBrowser;
    case apps::WindowMode::kTabbedWindow:
      return web_app::mojom::UserDisplayMode::kTabbed;
    case apps::WindowMode::kWindow:
      return web_app::mojom::UserDisplayMode::kStandalone;
    case apps::WindowMode::kUnknown:
      return web_app::mojom::UserDisplayMode::kBrowser;
  }
}

void IsolatedWebAppInstallationDone(
    const webapps::AppId& installed_app_id,
    StandaloneBrowserTestController::InstallIsolatedWebAppCallback callback,
    base::expected<web_app::InstallIsolatedWebAppCommandSuccess,
                   web_app::InstallIsolatedWebAppCommandError> install_result) {
  if (install_result.has_value()) {
    std::move(callback).Run(
        crosapi::mojom::InstallWebAppResult::NewAppId(installed_app_id));
  } else {
    std::move(callback).Run(
        crosapi::mojom::InstallWebAppResult::NewErrorMessage(
            install_result.error().message));
  }
}

void OnIsolatedWebAppUrlInfoCreated(
    const web_app::IsolatedWebAppInstallSource& install_source,
    StandaloneBrowserTestController::InstallIsolatedWebAppCallback callback,
    base::expected<web_app::IsolatedWebAppUrlInfo, std::string>
        iwa_url_info_expected) {
  ASSIGN_OR_RETURN(
      auto iwa_url_info, iwa_url_info_expected, [&](const std::string& error) {
        std::move(callback).Run(
            crosapi::mojom::InstallWebAppResult::NewErrorMessage(error));
      });

  if (!install_source.source().dev_mode()) {
    web_app::AddTrustedWebBundleIdForTesting(iwa_url_info.web_bundle_id());
  }

  Profile* profile = ProfileManager::GetPrimaryUserProfile();
  auto* provider = web_app::WebAppProvider::GetForWebApps(profile);
  provider->scheduler().InstallIsolatedWebApp(
      iwa_url_info, install_source, /*expected_version=*/std::nullopt,
      /*optional_keep_alive=*/nullptr, /*optional_profile_keep_alive=*/nullptr,
      base::BindOnce(&IsolatedWebAppInstallationDone, iwa_url_info.app_id(),
                     std::move(callback)));
}

class FakeExtensionPrinterHandler : public printing::PrinterHandler {
 public:
  FakeExtensionPrinterHandler() = default;
  FakeExtensionPrinterHandler(const FakeExtensionPrinterHandler&) = delete;
  FakeExtensionPrinterHandler& operator=(const FakeExtensionPrinterHandler&) =
      delete;
  ~FakeExtensionPrinterHandler() override = default;

  void Reset() override {}

  void StartGetPrinters(AddedPrintersCallback added_printers_callback,
                        GetPrintersDoneCallback done_callback) override {
    added_printers_callback.Run(base::test::ParseJsonList(R"(
      [ {
        "description": "A virtual printer for testing",
        "extensionId": "jbljdigmdjodgkcllikhggoepmmffbam",
        "extensionName": "Test Printer Provider",
        "id": "jbljdigmdjodgkcllikhggoepmmffbam:test-printer-01",
        "name": "Test Printer 01"
      }, {
        "description": "A virtual printer for testing",
        "extensionId": "jbljdigmdjodgkcllikhggoepmmffbam",
        "extensionName": "Test Printer Provider",
        "id": "jbljdigmdjodgkcllikhggoepmmffbam:test-printer-02",
        "name": "Test Printer 02"
      } ]
    )"));
    std::move(done_callback).Run();
  }

  void StartGetCapability(const std::string& destination_id,
                          GetCapabilityCallback callback) override {
    std::move(callback).Run(base::test::ParseJsonDict(R"(
    {
      "version": "1.0",
      "printer": {
        "supported_content_type": [
          {"content_type": "application/pdf"}
        ]
      }
    })"));
  }

  void StartGrantPrinterAccess(const std::string& printer_id,
                               GetPrinterInfoCallback callback) override {
    base::Value::Dict printer_info;
    printer_info.Set("printerId", printer_id);
    printer_info.Set("name", "Test Printer 01");

    std::move(callback).Run(std::move(printer_info));
  }

  void StartPrint(const std::u16string& job_title,
                  base::Value::Dict settings,
                  scoped_refptr<base::RefCountedMemory> print_data,
                  PrintCallback callback) override {
    // Simulate a successful print job. "OK" means successful.
    std::move(callback).Run(base::Value("OK"));
  }
};

}  // namespace

// With Lacros tts support enabled, all Lacros utterances will be sent to
// Ash to be processed by TtsController in Ash. When the utterance is spoken
// by a speech engine (provided by Ash or Lacros), we need to make sure that
// Tts events are routed back to the UtteranceEventDelegate in Lacros.
// This class can be set as UtteranceEventDelegate for Lacros Utterance used
// for testing.
class StandaloneBrowserTestController::LacrosUtteranceEventDelegate
    : public content::UtteranceEventDelegate {
 public:
  LacrosUtteranceEventDelegate(
      StandaloneBrowserTestController* controller,
      mojo::PendingRemote<crosapi::mojom::TtsUtteranceClient> client)
      : controller_(controller), client_(std::move(client)) {}

  LacrosUtteranceEventDelegate(const LacrosUtteranceEventDelegate&) = delete;
  LacrosUtteranceEventDelegate& operator=(const LacrosUtteranceEventDelegate&) =
      delete;
  ~LacrosUtteranceEventDelegate() override = default;

  // content::UtteranceEventDelegate methods:
  void OnTtsEvent(content::TtsUtterance* utterance,
                  content::TtsEventType event_type,
                  int char_index,
                  int char_length,
                  const std::string& error_message) override {
    // Forward the TtsEvent back to ash, so that ash browser test can verify
    // that TtsEvent has been routed to the UtteranceEventDelegate in Lacros.
    client_->OnTtsEvent(tts_crosapi_util::ToMojo(event_type), char_index,
                        char_length, error_message);

    if (utterance->IsFinished()) {
      controller_->OnUtteranceFinished(utterance->GetId());
    }
    // Note: |this| is deleted if utterance->IsFinished().
  }

 private:
  // |controller_| is guaranteed to be valid during the lifetime of this class.
  const raw_ptr<StandaloneBrowserTestController> controller_;
  mojo::Remote<crosapi::mojom::TtsUtteranceClient> client_;
};

StandaloneBrowserTestController::StandaloneBrowserTestController(
    mojo::Remote<crosapi::mojom::TestController>& test_controller) {
  CHECK_IS_TEST();
  test_controller->RegisterStandaloneBrowserTestController(
      controller_receiver_.BindNewPipeAndPassRemoteWithVersion());
  test_controller.FlushAsync();
}

StandaloneBrowserTestController::~StandaloneBrowserTestController() = default;

void StandaloneBrowserTestController::InstallWebApp(
    const std::string& start_url,
    apps::WindowMode window_mode,
    InstallWebAppCallback callback) {
  auto info =
      web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(GURL(start_url));
  info->title = u"Test Web App";
  info->display_mode = WindowModeToDisplayMode(window_mode);
  info->user_display_mode = WindowModeToUserDisplayMode(window_mode);
  Profile* profile = ProfileManager::GetPrimaryUserProfile();
  auto* provider = web_app::WebAppProvider::GetForWebApps(profile);
  provider->scheduler().InstallFromInfoNoIntegrationForTesting(
      std::move(info),
      /*overwrite_existing_manifest_fields=*/false,
      webapps::WebappInstallSource::SYNC,
      base::BindOnce(&StandaloneBrowserTestController::WebAppInstallationDone,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void StandaloneBrowserTestController::InstallUnpackedExtension(
    const std::string& path,
    InstallUnpackedExtensionCallback callback) {
  Profile* profile = ProfileManager::GetPrimaryUserProfile();
  extensions::ChromeTestExtensionLoader loader(profile);
  loader.LoadUnpackedExtensionAsync(
      base::FilePath{path},
      base::BindOnce([](const extensions::Extension* extension) {
        return extension->id();
      }).Then(std::move(callback)));
}

void StandaloneBrowserTestController::ObserveDomMessages(
    mojo::PendingRemote<crosapi::mojom::DomMessageObserver> observer,
    ObserveDomMessagesCallback callback) {
  dom_message_observer_.Bind(std::move(observer));
  dom_message_observer_.set_disconnect_handler(base::BindOnce(
      &StandaloneBrowserTestController::OnDomMessageObserverDisconnected,
      weak_ptr_factory_.GetWeakPtr()));

  ASSERT_FALSE(dom_message_queue_.has_value());
  dom_message_queue_.emplace();
  dom_message_queue_->SetOnMessageAvailableCallback(
      base::BindOnce(&StandaloneBrowserTestController::OnDomMessageQueueReady,
                     weak_ptr_factory_.GetWeakPtr()));

  std::move(callback).Run();
}

void StandaloneBrowserTestController::OnDomMessageObserverDisconnected() {
  dom_message_queue_.reset();
  dom_message_observer_.reset();
}

void StandaloneBrowserTestController::OnDomMessageQueueReady() {
  std::string message;
  ASSERT_TRUE(dom_message_queue_->PopMessage(&message));
  dom_message_observer_->OnMessage(message);

  dom_message_queue_->SetOnMessageAvailableCallback(
      base::BindOnce(&StandaloneBrowserTestController::OnDomMessageQueueReady,
                     weak_ptr_factory_.GetWeakPtr()));
}

void StandaloneBrowserTestController::InstallComponentExtension(
    const std::string& path,
    const std::string& extension_id,
    InstallComponentExtensionCallback callback) {
  Profile* profile = ProfileManager::GetPrimaryUserProfile();
  extensions::ExtensionService* service =
      extensions::ExtensionSystem::Get(profile)->extension_service();
  service->component_loader()->AddComponentFromDirWithManifestFilename(
      base::FilePath(path), extension_id, extensions::kManifestFilename,
      extensions::kManifestFilename, std::move(callback));
}

void StandaloneBrowserTestController::RemoveComponentExtension(
    const std::string& extension_id,
    RemoveComponentExtensionCallback callback) {
  Profile* profile = ProfileManager::GetPrimaryUserProfile();
  extensions::ExtensionSystem::Get(profile)
      ->extension_service()
      ->RemoveComponentExtension(extension_id);
  std::move(callback).Run();
}

void StandaloneBrowserTestController::LoadVpnExtension(
    const std::string& extension_name,
    LoadVpnExtensionCallback callback) {
  std::string error;
  auto extension = extensions::Extension::Create(
      base::FilePath(), extensions::mojom::ManifestLocation::kUnpacked,
      CreateVpnExtensionManifest(extension_name),
      extensions::Extension::NO_FLAGS, &error);
  if (!error.empty()) {
    std::move(callback).Run(error);
    return;
  }

  auto* extension_registry = extensions::ExtensionRegistry::Get(
      ProfileManager::GetPrimaryUserProfile());
  extension_registry->AddEnabled(extension);
  extension_registry->TriggerOnLoaded(extension.get());

  std::move(callback).Run(extension->id());
}

void StandaloneBrowserTestController::GetTtsVoices(
    GetTtsVoicesCallback callback) {
  std::vector<content::VoiceData> voices;
  tts_crosapi_util::GetAllVoicesForTesting(  // IN-TEST
      ProfileManager::GetActiveUserProfile(), GURL(), &voices);

  std::vector<crosapi::mojom::TtsVoicePtr> mojo_voices;
  for (const auto& voice : voices) {
    mojo_voices.push_back(tts_crosapi_util::ToMojo(voice));
  }

  std::move(callback).Run(std::move(mojo_voices));
}

void StandaloneBrowserTestController::TtsSpeak(
    crosapi::mojom::TtsUtterancePtr mojo_utterance,
    mojo::PendingRemote<crosapi::mojom::TtsUtteranceClient> utterance_client) {
  std::unique_ptr<content::TtsUtterance> lacros_utterance =
      tts_crosapi_util::CreateUtteranceFromMojo(
          mojo_utterance, /*should_always_be_spoken=*/true);
  auto event_delegate = std::make_unique<LacrosUtteranceEventDelegate>(
      this, std::move(utterance_client));
  lacros_utterance->SetEventDelegate(event_delegate.get());
  lacros_utterance_event_delegates_.emplace(lacros_utterance->GetId(),
                                            std ::move(event_delegate));
  tts_crosapi_util::SpeakForTesting(std::move(lacros_utterance));
}

void StandaloneBrowserTestController::InstallSubApp(
    const webapps::AppId& parent_app_id,
    const std::string& sub_app_path,
    InstallSubAppCallback callback) {
  GURL parent_app_url(
      base::StrCat({chrome::kIsolatedAppScheme, url::kStandardSchemeSeparator,
                    parent_app_id}));

  GURL start_url = parent_app_url.Resolve(sub_app_path);
  auto info =
      web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
  info->parent_app_id = parent_app_id;
  info->parent_app_manifest_id = parent_app_url;
  info->title = u"Sub App";

  Profile* profile = ProfileManager::GetPrimaryUserProfile();
  auto* provider = web_app::WebAppProvider::GetForWebApps(profile);
  provider->scheduler().InstallFromInfoNoIntegrationForTesting(
      std::move(info),
      /*overwrite_existing_manifest_fields=*/false,
      webapps::WebappInstallSource::SUB_APP,
      base::BindOnce(&StandaloneBrowserTestController::WebAppInstallationDone,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void StandaloneBrowserTestController::InstallIsolatedWebApp(
    crosapi::mojom::IsolatedWebAppLocationPtr location,
    bool dev_mode,
    InstallIsolatedWebAppCallback callback) {
  web_app::IsolatedWebAppInstallSource install_source = ([&]() {
    if (dev_mode) {
      if (location->is_bundle_path()) {
        return web_app::IsolatedWebAppInstallSource::FromDevUi(
            web_app::IwaSourceBundleDevModeWithFileOp(
                base::FilePath(location->get_bundle_path()),
                web_app::IwaSourceBundleDevFileOp::kCopy));
      } else {
        return web_app::IsolatedWebAppInstallSource::FromDevUi(
            web_app::IwaSourceProxy(
                url::Origin::Create(location->get_proxy_origin())));
      }
    } else {
      return web_app::IsolatedWebAppInstallSource::FromGraphicalInstaller(
          web_app::IwaSourceBundleProdModeWithFileOp(
              base::FilePath(location->get_bundle_path()),
              web_app::IwaSourceBundleProdFileOp::kCopy));
    }
  })();

  web_app::IsolatedWebAppUrlInfo::CreateFromIsolatedWebAppSource(
      install_source.source(),
      base::BindOnce(&OnIsolatedWebAppUrlInfoCreated, install_source,
                     std::move(callback)));
}

void StandaloneBrowserTestController::SetWebAppSettingsPref(
    const std::string& policy,
    SetWebAppSettingsPrefCallback callback) {
  CHECK(callback);

  auto result = base::JSONReader::ReadAndReturnValueWithError(
      policy, base::JSONParserOptions::JSON_ALLOW_TRAILING_COMMAS);
  if (!result.has_value()) {
    std::move(callback).Run(/*success=*/false);
    return;
  }
  if (!result->is_list()) {
    std::move(callback).Run(/*success=*/false);
    return;
  }

  ProfileManager::GetPrimaryUserProfile()->GetPrefs()->SetList(
      prefs::kWebAppSettings, std::move(*result).TakeList());

  std::move(callback).Run(/*success=*/true);
}

void StandaloneBrowserTestController::SetWebAppInstallForceListPref(
    const std::string& policy,
    SetWebAppInstallForceListPrefCallback callback) {
  CHECK(callback);

  auto result = base::JSONReader::ReadAndReturnValueWithError(
      policy, base::JSONParserOptions::JSON_ALLOW_TRAILING_COMMAS);
  if (!result.has_value()) {
    std::move(callback).Run(/*success=*/false);
    return;
  }
  if (!result->is_list()) {
    std::move(callback).Run(/*success=*/false);
    return;
  }

  ProfileManager::GetPrimaryUserProfile()->GetPrefs()->SetList(
      prefs::kWebAppInstallForceList, std::move(*result).TakeList());

  std::move(callback).Run(/*success=*/true);
}

// Uses a fake extension printer handler to process printing requests from ash
// browser tests.
void StandaloneBrowserTestController::SetFakeExtensionPrinterHandler(
    SetFakeExtensionPrinterHandlerCallback callback) {
  printing::ExtensionPrinterServiceProviderLacros::GetForBrowserContext(
      ProfileManager::GetPrimaryUserProfile())
      ->SetPrinterHandlerForTesting(
          std::make_unique<FakeExtensionPrinterHandler>());
  std::move(callback).Run();
}

void StandaloneBrowserTestController::OnUtteranceFinished(int utterance_id) {
  // Delete the utterace event delegate object when the utterance is finished.
  lacros_utterance_event_delegates_.erase(utterance_id);
}

void StandaloneBrowserTestController::GetExtensionKeeplist(
    GetExtensionKeeplistCallback callback) {
  auto mojo_keeplist = crosapi::mojom::ExtensionKeepList::New();

  for (const auto& id :
       extensions::GetExtensionsRunInOSAndStandaloneBrowser()) {
    mojo_keeplist->extensions_run_in_os_and_standalonebrowser.push_back(
        std::string(id));
  }

  for (const auto& id :
       extensions::GetExtensionAppsRunInOSAndStandaloneBrowser()) {
    mojo_keeplist->extension_apps_run_in_os_and_standalonebrowser.push_back(
        std::string(id));
  }

  for (const auto& id : extensions::GetExtensionsRunInOSOnly()) {
    mojo_keeplist->extensions_run_in_os_only.push_back(std::string(id));
  }

  for (const auto& id : extensions::GetExtensionAppsRunInOSOnly()) {
    mojo_keeplist->extension_apps_run_in_os_only.push_back(std::string(id));
  }

  std::move(callback).Run(std::move(mojo_keeplist));
}

void StandaloneBrowserTestController::WebAppInstallationDone(
    InstallWebAppCallback callback,
    const webapps::AppId& installed_app_id,
    webapps::InstallResultCode code) {
  std::move(callback).Run(code == webapps::InstallResultCode::kSuccessNewInstall
                              ? installed_app_id
                              : "");
}

base::Value::Dict StandaloneBrowserTestController::CreateVpnExtensionManifest(
    const std::string& extension_name) {
  base::Value::Dict manifest;

  manifest.Set(extensions::manifest_keys::kName, extension_name);
  manifest.Set(extensions::manifest_keys::kVersion, "1.0");
  manifest.Set(extensions::manifest_keys::kManifestVersion, 2);

  base::Value::List permissions;
  permissions.Append("vpnProvider");
  manifest.Set(extensions::manifest_keys::kPermissions, std::move(permissions));

  return manifest;
}