chromium/chrome/browser/extensions/api/vpn_provider/vpn_provider_apitest.cc

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include <stddef.h>
#include <stdint.h>

#include <optional>

#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/test/test_future.h"
#include "base/types/cxx23_to_underlying.h"
#include "chrome/browser/chromeos/extensions/vpn_provider/vpn_provider_api.h"
#include "chrome/browser/chromeos/extensions/vpn_provider/vpn_service_factory.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/api/vpn_provider.h"
#include "chromeos/ash/components/network/shill_property_handler.h"
#include "chromeos/crosapi/mojom/vpn_service.mojom.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/pepper_vpn_provider_resource_host_proxy.h"
#include "content/public/browser/vpn_service_proxy.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/api_test_utils.h"
#include "extensions/common/extension.h"
#include "extensions/test/result_catcher.h"
#include "testing/gmock/include/gmock/gmock.h"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/crosapi/vpn_service_ash.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chromeos/ash/components/dbus/shill/fake_shill_third_party_vpn_driver_client.h"
#include "chromeos/ash/components/dbus/shill/shill_manager_client.h"
#include "chromeos/ash/components/dbus/shill/shill_profile_client.h"
#include "chromeos/ash/components/network/network_configuration_handler.h"
#include "third_party/cros_system_api/dbus/service_constants.h"
#endif

#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/crosapi/mojom/test_controller.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#endif

namespace chromeos {

namespace {

namespace api_vpn = extensions::api::vpn_provider;

const char kTestConfig[] = "testconfig";
const char kPacket[] = "feebdaed";

#if BUILDFLAG(IS_CHROMEOS_ASH)
const char kNetworkProfilePath[] = "/network/test";
const char* kParameterValues[] = {"10.10.10.10",
                                  "24",
                                  "63.145.213.129/32 63.145.212.0/24",
                                  "0.0.0.0/0 63.145.212.128/25",
                                  "8.8.8.8",
                                  "1600",
                                  "10.10.10.255",
                                  "foo:bar"};
const char* kParameterKeys[] = {shill::kAddressParameterThirdPartyVpn,
                                shill::kSubnetPrefixParameterThirdPartyVpn,
                                shill::kExclusionListParameterThirdPartyVpn,
                                shill::kInclusionListParameterThirdPartyVpn,
                                shill::kDnsServersParameterThirdPartyVpn,
                                shill::kMtuParameterThirdPartyVpn,
                                shill::kBroadcastAddressParameterThirdPartyVpn,
                                shill::kDomainSearchParameterThirdPartyVpn};

void DoNothingFailureCallback(const std::string& error_name) {
  FAIL();
}

void DoNothingSuccessCallback(const std::string& service_path,
                              const std::string& guid) {}

#endif

}  // namespace

#if BUILDFLAG(IS_CHROMEOS_ASH)
// Records the number of calls and their parameters. Always replies successfully
// to calls.
class TestShillThirdPartyVpnDriverClient
    : public ash::FakeShillThirdPartyVpnDriverClient {
 public:
  void SetParameters(const std::string& object_path_value,
                     const base::Value::Dict& parameters,
                     StringCallback callback,
                     ErrorCallback error_callback) override {
    set_parameters_counter_++;
    parameters_ = parameters.Clone();
    FakeShillThirdPartyVpnDriverClient::SetParameters(
        object_path_value, parameters, std::move(callback),
        std::move(error_callback));
  }

  void UpdateConnectionState(const std::string& object_path_value,
                             const uint32_t connection_state,
                             base::OnceClosure callback,
                             ErrorCallback error_callback) override {
    update_connection_state_counter_++;
    connection_state_ = connection_state;
    FakeShillThirdPartyVpnDriverClient::UpdateConnectionState(
        object_path_value, connection_state, std::move(callback),
        std::move(error_callback));
  }

  void SendPacket(const std::string& object_path_value,
                  const std::vector<char>& ip_packet,
                  base::OnceClosure callback,
                  ErrorCallback error_callback) override {
    send_packet_counter_++;
    ip_packet_ = ip_packet;
    FakeShillThirdPartyVpnDriverClient::SendPacket(object_path_value, ip_packet,
                                                   std::move(callback),
                                                   std::move(error_callback));
  }

  int set_parameters_counter_ = 0;
  base::Value::Dict parameters_;
  int update_connection_state_counter_ = 0;
  uint32_t connection_state_;
  int send_packet_counter_ = 0;
  std::vector<char> ip_packet_;
};
#endif

class VpnProviderApiTestBase : public extensions::ExtensionApiTest {
 public:
  // extensions::ExtensionApiTest
  void SetUpOnMainThread() override {
    extensions::ExtensionApiTest::SetUpOnMainThread();
    LoadVpnExtension();
  }

  bool RunTest(const std::string& test_name) {
    DCHECK(extension_);
    const std::string extension_url = "basic.html?#" + test_name;
    return RunExtensionTest("vpn_provider",
                            {.extension_url = extension_url.c_str()});
  }

  const std::string& extension_id() const {
    DCHECK(extension_id_);
    return *extension_id_;
  }

  chromeos::VpnServiceInterface* service() {
    return chromeos::VpnServiceFactory::GetForBrowserContext(profile());
  }

  virtual crosapi::mojom::VpnService* service_remote() const = 0;

  virtual void OnPlatformMessage(const std::string& configuration_name,
                                 api_vpn::PlatformMessage) = 0;
  virtual void OnPacketReceived(const std::string& configuration_name,
                                const std::vector<char>& data) = 0;

 protected:
  void LoadVpnExtension() {
    DCHECK(!extension_);
    extension_ = LoadExtension(test_data_dir_.AppendASCII("vpn_provider"));
    extension_id_ = extension_->id();
  }

  raw_ptr<const extensions::Extension, DanglingUntriaged> extension_ = nullptr;
  std::optional<std::string> extension_id_;
};

#if BUILDFLAG(IS_CHROMEOS_LACROS)
class VpnProviderApiTestLacros : public VpnProviderApiTestBase {
 public:
  // VpnProviderApiTestBase:
  void TearDownOnMainThread() override {
    UnloadExtension(extension_id());
    VpnProviderApiTestBase::TearDownOnMainThread();
  }
  crosapi::mojom::VpnService* service_remote() const override {
    return chromeos::LacrosService::Get()
        ->GetRemote<crosapi::mojom::VpnService>()
        .get();
  }
  void OnPlatformMessage(const std::string& configuration_name,
                         api_vpn::PlatformMessage message) override {
    controller_->OnPlatformMessage(extension_id(), configuration_name,
                                   base::to_underlying(message));
  }
  void OnPacketReceived(const std::string& configuration_name,
                        const std::vector<char>& data) override {
    controller_->OnPacketReceived(
        extension_id(), configuration_name,
        std::vector<uint8_t>(data.begin(), data.end()));
  }

  bool InitTestShillController() {
    auto* service = chromeos::LacrosService::Get();
    if (!service->IsAvailable<crosapi::mojom::TestController>() ||
        service->GetInterfaceVersion<crosapi::mojom::TestController>() <
            static_cast<int>(crosapi::mojom::TestController::MethodMinVersions::
                                 kBindTestShillControllerMinVersion)) {
      LOG(ERROR) << "Unsupported ash version.";
      return false;
    }
    base::test::TestFuture<void> future;
    service->GetRemote<crosapi::mojom::TestController>()
        ->BindTestShillController(controller_.BindNewPipeAndPassReceiver(),
                                  future.GetCallback());
    EXPECT_TRUE(future.Wait());
    return true;
  }

 protected:
  mojo::Remote<crosapi::mojom::TestShillController> controller_;
};
#else
class VpnProviderApiTestAsh : public VpnProviderApiTestBase {
 public:
  // VpnProviderApiTestBase:
  void SetUpInProcessBrowserTestFixture() override {
    VpnProviderApiTestBase::SetUpInProcessBrowserTestFixture();
    // Destroy the existing client and create a test specific fake client. It
    // will be destroyed in ChromeBrowserMain.
    test_client_ = new TestShillThirdPartyVpnDriverClient();
  }
  void SetUpOnMainThread() override {
    VpnProviderApiTestBase::SetUpOnMainThread();
    AddNetworkProfileForUser();
  }
  crosapi::mojom::VpnService* service_remote() const override {
    return GetVpnServiceAsh();
  }
  void OnPlatformMessage(const std::string& configuration_name,
                         api_vpn::PlatformMessage message) override {
    test_client_->OnPlatformMessage(
        shill::kObjectPathBase + GetKey(configuration_name),
        base::to_underlying(message));
  }
  void OnPacketReceived(const std::string& configuration_name,
                        const std::vector<char>& data) override {
    test_client_->OnPacketReceived(
        shill::kObjectPathBase + GetKey(configuration_name), data);
  }

  std::string GetKey(const std::string& configuration_name) const {
    return crosapi::VpnServiceForExtensionAsh::GetKey(extension_id(),
                                                      configuration_name);
  }

  bool DoesConfigExist(const std::string& configuration_name) const {
    const auto& mapping = GetVpnServiceAsh()->extension_id_to_service_;
    if (!base::Contains(mapping, extension_id())) {
      return false;
    }
    return base::Contains(mapping.at(extension_id())->key_to_configuration_map_,
                          GetKey(configuration_name));
  }

  bool IsConfigConnected() const {
    const auto& mapping = GetVpnServiceAsh()->extension_id_to_service_;
    if (!base::Contains(mapping, extension_id())) {
      return false;
    }
    return mapping.at(extension_id())->OwnsActiveConfiguration();
  }

  std::string GetSingleServicePath() {
    std::vector<std::string> service_paths;
    for (const auto& [extension_id, service] :
         GetVpnServiceAsh()->extension_id_to_service_) {
      const auto& service_path_map =
          service->service_path_to_configuration_map_;
      if (service_path_map.empty()) {
        continue;
      }
      DCHECK_EQ(service_path_map.size(), 1U);
      service_paths.push_back(service_path_map.begin()->first);
    }
    EXPECT_EQ(service_paths.size(), 1U);
    return service_paths[0];
  }

  bool CreateConfigForTest(const std::string& name) {
    scoped_refptr<extensions::VpnProviderCreateConfigFunction> create(
        new extensions::VpnProviderCreateConfigFunction());

    create->set_extension(GetSingleLoadedExtension());
    return extensions::api_test_utils::RunFunction(
        create.get(), "[\"" + name + "\"]", profile());
  }

  bool DestroyConfigForTest(const std::string& name) {
    scoped_refptr<extensions::VpnProviderDestroyConfigFunction> destroy(
        new extensions::VpnProviderDestroyConfigFunction());

    destroy->set_extension(GetSingleLoadedExtension());
    return extensions::api_test_utils::RunFunction(
        destroy.get(), "[\"" + name + "\"]", profile());
  }

  void TriggerInternalRemove() {
    ash::NetworkHandler::Get()
        ->network_configuration_handler()
        ->RemoveConfiguration(
            GetSingleServicePath(), /*remove_confirmer=*/std::nullopt,
            base::DoNothing(), base::BindOnce(DoNothingFailureCallback));
  }

  bool HasService(const std::string& service_path) const {
    std::string profile_path;
    std::optional<base::Value::Dict> properties =
        ash::ShillProfileClient::Get()->GetTestInterface()->GetService(
            service_path, &profile_path);
    return properties.has_value();
  }

  void SendPlatformError(const std::string& extension_id,
                         const std::string& configuration_name,
                         const std::string& error_message) {
    const auto& mapping = GetVpnServiceAsh()->extension_id_to_service_;
    DCHECK(base::Contains(mapping, extension_id));
    auto* service = mapping.at(extension_id).get();
    service->DispatchOnPlatformMessageEvent(
        configuration_name,
        base::to_underlying(api_vpn::PlatformMessage::kError), error_message);
  }

  void ClearNetworkProfiles() {
    ash::ShillProfileClient::Get()->GetTestInterface()->ClearProfiles();
    // ShillProfileClient doesn't notify NetworkProfileHandler that profiles got
    // cleared, therefore we have to call ShillManagerClient explicitly.
    ash::ShillManagerClient::Get()->GetTestInterface()->ClearProfiles();
  }

 protected:
  void AddNetworkProfileForUser() {
    ash::ShillProfileClient::Get()->GetTestInterface()->AddProfile(
        kNetworkProfilePath,
        ash::ProfileHelper::GetUserIdHashFromProfile(profile()));
    content::RunAllPendingInMessageLoop();
  }

  static crosapi::VpnServiceAsh* GetVpnServiceAsh() {
    return crosapi::CrosapiManager::Get()->crosapi_ash()->vpn_service_ash();
  }

  raw_ptr<TestShillThirdPartyVpnDriverClient, DanglingUntriaged> test_client_ =
      nullptr;  // Unowned
};
#endif

#if BUILDFLAG(IS_CHROMEOS_LACROS)
using VpnProviderApiTest = VpnProviderApiTestLacros;
#else
using VpnProviderApiTest = VpnProviderApiTestAsh;
#endif

////////////////////////////
// Ash-specific tests.
////////////////////////////

#if BUILDFLAG(IS_CHROMEOS_ASH)
IN_PROC_BROWSER_TEST_F(VpnProviderApiTest, CreateConfigWithoutNetworkProfile) {
  ClearNetworkProfiles();
  EXPECT_TRUE(RunTest("createConfigWithoutNetworkProfile"));
}

IN_PROC_BROWSER_TEST_F(VpnProviderApiTest, CreateConfig) {
  EXPECT_TRUE(RunTest("createConfigSuccess"));
  EXPECT_TRUE(DoesConfigExist(kTestConfig));
  EXPECT_TRUE(HasService(GetSingleServicePath()));
}

IN_PROC_BROWSER_TEST_F(VpnProviderApiTest, DestroyConfig) {
  EXPECT_TRUE(CreateConfigForTest(kTestConfig));
  EXPECT_TRUE(DoesConfigExist(kTestConfig));
  const std::string service_path = GetSingleServicePath();
  EXPECT_TRUE(HasService(service_path));

  EXPECT_TRUE(RunTest("destroyConfigSuccess"));
  EXPECT_FALSE(DoesConfigExist(kTestConfig));
  EXPECT_FALSE(HasService(service_path));
}

IN_PROC_BROWSER_TEST_F(VpnProviderApiTest, DestroyConnectedConfig) {
  EXPECT_TRUE(CreateConfigForTest(kTestConfig));
  EXPECT_TRUE(DoesConfigExist(kTestConfig));
  const std::string service_path = GetSingleServicePath();
  EXPECT_TRUE(HasService(service_path));
  EXPECT_FALSE(IsConfigConnected());

  OnPlatformMessage(kTestConfig, api_vpn::PlatformMessage::kConnected);
  EXPECT_TRUE(IsConfigConnected());

  EXPECT_TRUE(RunTest("destroyConnectedConfigSetup"));

  extensions::ResultCatcher catcher;

  EXPECT_TRUE(DestroyConfigForTest(kTestConfig));
  EXPECT_FALSE(DoesConfigExist(kTestConfig));
  EXPECT_FALSE(HasService(service_path));

  ASSERT_TRUE(catcher.GetNextResult());
}

IN_PROC_BROWSER_TEST_F(VpnProviderApiTest, ConfigInternalRemove) {
  EXPECT_TRUE(RunTest("configInternalRemove"));
  EXPECT_TRUE(DoesConfigExist(kTestConfig));

  extensions::ResultCatcher catcher;
  TriggerInternalRemove();
  ASSERT_TRUE(catcher.GetNextResult());
  EXPECT_FALSE(DoesConfigExist(kTestConfig));
}

IN_PROC_BROWSER_TEST_F(VpnProviderApiTest, CheckEvents) {
  EXPECT_TRUE(RunTest("expectEvents"));
  EXPECT_TRUE(DoesConfigExist(kTestConfig));

  extensions::ResultCatcher catcher;
  SendPlatformError(extension_id(), kTestConfig, "error_message");
  service()->SendShowAddDialogToExtension(extension_id());
  service()->SendShowConfigureDialogToExtension(extension_id(), kTestConfig);
  EXPECT_TRUE(catcher.GetNextResult());
}

IN_PROC_BROWSER_TEST_F(VpnProviderApiTest, ConfigPersistence) {
  EXPECT_FALSE(DoesConfigExist(kTestConfig));

  base::Value::Dict properties;
  properties.Set(shill::kTypeProperty, shill::kTypeVPN);
  properties.Set(shill::kNameProperty, kTestConfig);
  properties.Set(shill::kProviderHostProperty, extension_id());
  properties.Set(shill::kObjectPathSuffixProperty, GetKey(kTestConfig));
  properties.Set(shill::kProviderTypeProperty, shill::kProviderThirdPartyVpn);
  properties.Set(shill::kProfileProperty, kNetworkProfilePath);

  ash::NetworkHandler::Get()
      ->network_configuration_handler()
      ->CreateShillConfiguration(std::move(properties),
                                 base::BindOnce(DoNothingSuccessCallback),
                                 base::BindOnce(DoNothingFailureCallback));
  content::RunAllPendingInMessageLoop();
  EXPECT_TRUE(DoesConfigExist(kTestConfig));
}

IN_PROC_BROWSER_TEST_F(VpnProviderApiTest, CreateUninstall) {
  EXPECT_TRUE(RunTest("createConfigSuccess"));
  EXPECT_TRUE(DoesConfigExist(kTestConfig));

  const std::string service_path = GetSingleServicePath();
  EXPECT_TRUE(HasService(service_path));

  UninstallExtension(extension_id());
  content::RunAllPendingInMessageLoop();
  EXPECT_FALSE(DoesConfigExist(kTestConfig));
  EXPECT_FALSE(HasService(service_path));
}

IN_PROC_BROWSER_TEST_F(VpnProviderApiTest, CreateDisable) {
  EXPECT_TRUE(RunTest("createConfigSuccess"));
  EXPECT_TRUE(DoesConfigExist(kTestConfig));

  const std::string service_path = GetSingleServicePath();
  EXPECT_TRUE(HasService(service_path));

  extensions::ExtensionService* extension_service =
      extensions::ExtensionSystem::Get(profile())->extension_service();
  extension_service->DisableExtension(
      extension_id(), extensions::disable_reason::DISABLE_USER_ACTION);
  content::RunAllPendingInMessageLoop();
  EXPECT_FALSE(DoesConfigExist(kTestConfig));
  EXPECT_FALSE(HasService(service_path));
}

IN_PROC_BROWSER_TEST_F(VpnProviderApiTest, CreateBlocklist) {
  EXPECT_TRUE(RunTest("createConfigSuccess"));
  EXPECT_TRUE(DoesConfigExist(kTestConfig));

  const std::string service_path = GetSingleServicePath();
  EXPECT_TRUE(HasService(service_path));

  extensions::ExtensionService* extension_service =
      extensions::ExtensionSystem::Get(profile())->extension_service();
  extension_service->BlocklistExtensionForTest(extension_id());
  content::RunAllPendingInMessageLoop();
  EXPECT_FALSE(DoesConfigExist(kTestConfig));
  EXPECT_FALSE(HasService(service_path));
}
#endif

////////////////////////////
// Ash/lacros shared tests.
////////////////////////////

IN_PROC_BROWSER_TEST_F(VpnProviderApiTest, ComboSuite) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  if (!InitTestShillController()) {
    GTEST_SKIP() << "Unsupported ash version.";
  }
#endif

  EXPECT_TRUE(RunTest("comboSuite"));
}

IN_PROC_BROWSER_TEST_F(VpnProviderApiTest, VpnSuccess) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  if (!InitTestShillController()) {
    GTEST_SKIP() << "Unsupported ash version.";
  }
#endif

  EXPECT_TRUE(RunTest("createConfigConnectAndDisconnect"));

#if BUILDFLAG(IS_CHROMEOS_ASH)
  EXPECT_TRUE(DoesConfigExist(kTestConfig));
  EXPECT_TRUE(HasService(GetSingleServicePath()));
  EXPECT_FALSE(IsConfigConnected());
  EXPECT_EQ(0, test_client_->set_parameters_counter_);
  EXPECT_EQ(0, test_client_->update_connection_state_counter_);
  EXPECT_EQ(0, test_client_->send_packet_counter_);
#endif

  extensions::ResultCatcher catcher;
  OnPlatformMessage(kTestConfig, api_vpn::PlatformMessage::kConnected);
  ASSERT_TRUE(catcher.GetNextResult());

#if BUILDFLAG(IS_CHROMEOS_ASH)
  EXPECT_TRUE(IsConfigConnected());
  EXPECT_EQ(1, test_client_->set_parameters_counter_);
  EXPECT_EQ(1, test_client_->update_connection_state_counter_);
  EXPECT_EQ(1, test_client_->send_packet_counter_);
  EXPECT_EQ(base::to_underlying(api_vpn::VpnConnectionState::kConnected),
            test_client_->update_connection_state_counter_);
  for (size_t i = 0; i < std::size(kParameterValues); ++i) {
    const std::string* value =
        test_client_->parameters_.FindString(kParameterKeys[i]);
    ASSERT_TRUE(value);
    EXPECT_EQ(kParameterValues[i], *value);
  }
  std::vector<char> received_packet(std::begin(kPacket),
                                    std::prev(std::end(kPacket)));
  EXPECT_EQ(received_packet, test_client_->ip_packet_);
#endif

  std::vector<char> packet(++std::rbegin(kPacket), std::rend(kPacket));
  OnPacketReceived(kTestConfig, packet);
  ASSERT_TRUE(catcher.GetNextResult());

  OnPlatformMessage(kTestConfig, api_vpn::PlatformMessage::kDisconnected);
  ASSERT_TRUE(catcher.GetNextResult());

#if BUILDFLAG(IS_CHROMEOS_ASH)
  EXPECT_FALSE(IsConfigConnected());
#endif
}

class FakePepperVpnProviderResourceHostProxy
    : public content::PepperVpnProviderResourceHostProxy {
 public:
  FakePepperVpnProviderResourceHostProxy(
      base::test::TestFuture<bool>* unbind,
      base::test::TestFuture<std::vector<char>>* data)
      : unbind_(unbind), data_(data) {}

  void SendOnUnbind() override { unbind_->SetValue(true); }

  void SendOnPacketReceived(const std::vector<char>& data) override {
    data_->SetValue(data);
  }

 private:
  raw_ptr<base::test::TestFuture<bool>> unbind_;
  raw_ptr<base::test::TestFuture<std::vector<char>>> data_;
};

IN_PROC_BROWSER_TEST_F(VpnProviderApiTest, PepperProxy) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  if (!InitTestShillController()) {
    GTEST_SKIP() << "Unsupported ash version.";
  }
#endif

  base::test::TestFuture<bool> unbind;
  base::test::TestFuture<std::vector<char>> data;
  // This class will be used as a receiver for mojo::SelfOwnedReceiver.
  // Therefore it's unsafe to keep these TestFuture-s as members (especially
  // |unbind|).
  auto pepper_proxy =
      std::make_unique<FakePepperVpnProviderResourceHostProxy>(&unbind, &data);

  extensions::ResultCatcher catcher;

  // Create config and imitate the platform sending a
  // PLATFORM_MESSAGE_CONNECTED.
  EXPECT_TRUE(RunTest("createConfigConnectForBind"));
  ASSERT_TRUE(catcher.GetNextResult());
  OnPlatformMessage(kTestConfig, api_vpn::PlatformMessage::kConnected);
  ASSERT_TRUE(catcher.GetNextResult());

  // Synchronously bind the fake pepper proxy.
  base::RunLoop run_loop;
  service()->GetVpnServiceProxy()->Bind(
      extension_id(), {}, kTestConfig, run_loop.QuitClosure(),
      base::DoNothing(), std::move(pepper_proxy));
  run_loop.Run();

  // Assert that packets are routed through the proxy.
  OnPacketReceived(kTestConfig,
                   std::vector<char>{std::begin(kPacket), std::end(kPacket)});
  ASSERT_TRUE(data.Wait());

  // Assert that pepper proxy receives an OnUnbind event on
  // PLATFORM_MESSAGE_DISCONNECTED.
  OnPlatformMessage(kTestConfig, api_vpn::PlatformMessage::kDisconnected);
  ASSERT_TRUE(catcher.GetNextResult());
  ASSERT_TRUE(unbind.Wait());
}

class TestEventObserverForExtension
    : public crosapi::mojom::EventObserverForExtension {
 public:
  // crosapi::mojom::EventObserverForExtension:
  void OnAddDialog() override {}
  void OnConfigureDialog(const std::string& configuration_name) override {}
  void OnConfigRemoved(const std::string& configuration_name) override {}
  void OnPlatformMessage(const std::string& configuration_name,
                         int32_t platform_message,
                         const std::optional<std::string>& error) override {}
  void OnPacketReceived(const std::vector<uint8_t>& data) override {}
};

// Tests that the per-extension crosapi connection between ash and browser
// is initialized by the moment ash decides to send a platform message to the
// browser.
IN_PROC_BROWSER_TEST_F(VpnProviderApiTest, PlatformMessage) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  if (!InitTestShillController()) {
    GTEST_SKIP() << "Unsupported ash version.";
  }
#endif

  auto test_observer = std::make_unique<TestEventObserverForExtension>();
  mojo::Remote<crosapi::mojom::VpnServiceForExtension> remote;
  mojo::Receiver<crosapi::mojom::EventObserverForExtension> receiver{
      test_observer.get()};
  service_remote()->RegisterVpnServiceForExtension(
      extension_id(), remote.BindNewPipeAndPassReceiver(),
      receiver.BindNewPipeAndPassRemote());

  base::test::TestFuture<crosapi::mojom::VpnErrorResponsePtr> future;
  remote->CreateConfiguration(kTestConfig, future.GetCallback());
  auto error = future.Take();
  ASSERT_FALSE(error) << "CreateConfiguration failed with |message| = "
                      << error->message.value_or(std::string{});

  extensions::ResultCatcher catcher;
  EXPECT_TRUE(RunTest("platformMessage"));
  ASSERT_TRUE(catcher.GetNextResult());

  OnPlatformMessage(kTestConfig, api_vpn::PlatformMessage::kConnected);
  ASSERT_TRUE(catcher.GetNextResult());
  OnPlatformMessage(kTestConfig, api_vpn::PlatformMessage::kDisconnected);
  ASSERT_TRUE(catcher.GetNextResult());
}

}  // namespace chromeos