blob: a04346032a59fc4cf09107a4bfa3ea0c7fdf23d0 [file] [log] [blame]
// Copyright 2022 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/hid/chrome_hid_delegate.h"
#include <memory>
#include <string_view>
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/test_future.h"
#include "base/uuid.h"
#include "build/build_config.h"
#include "chrome/browser/hid/hid_chooser_context.h"
#include "chrome/browser/hid/hid_chooser_context_factory.h"
#include "chrome/browser/hid/hid_connection_tracker.h"
#include "chrome/browser/hid/hid_connection_tracker_factory.h"
#include "chrome/browser/prefs/browser_prefs.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/scoped_testing_local_state.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/keyed_service/core/keyed_service.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/embedded_worker_instance_test_harness.h"
#include "content/public/test/web_contents_tester.h"
#include "extensions/buildflags/buildflags.h"
#include "google_apis/gaia/gaia_id.h"
#include "services/device/public/cpp/test/fake_hid_manager.h"
#include "services/device/public/cpp/test/hid_test_util.h"
#include "services/device/public/cpp/test/test_report_descriptors.h"
#include "services/device/public/mojom/hid.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/hid/hid.mojom.h"
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "base/command_line.h"
#include "base/values.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/scoped_user_manager.h"
#endif
namespace {
using ::base::test::RunClosure;
using ::base::test::TestFuture;
using ::testing::ElementsAre;
using ::testing::NiceMock;
using ::testing::UnorderedElementsAre;
constexpr std::string_view kDefaultTestUrl{"https://www.google.com"};
constexpr std::string_view kCrossOriginTestUrl{"https://www.chromium.org"};
constexpr char kTestUserEmail[] = "user@example.com";
#if BUILDFLAG(ENABLE_EXTENSIONS)
constexpr std::string_view kPrivilegedExtensionId{
"ckcendljdlmgnhghiaomidhiiclmapok"};
constexpr std::string_view kExtensionDocumentFileName{"index.html"};
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
MATCHER_P(HasGuid, matcher, "") {
return ExplainMatchResult(matcher, arg->guid, result_listener);
}
device::mojom::HidDeviceInfoPtr CreateFakeDevice() {
auto device = device::CreateDeviceFromReportDescriptor(
/*vendor_id=*/0x1234, /*product_id=*/0xabcd,
device::TestReportDescriptors::JabraLink380c());
// Ensure `serial_number` is unique.
device->serial_number = device->guid;
return device;
}
// Create a partially-initialized device.
device::mojom::HidDeviceInfoPtr CreateIncompleteFakeDevice() {
auto device = CreateFakeDevice();
EXPECT_GT(device->collections.size(), 1u);
device->collections.pop_back();
return device;
}
device::mojom::HidDeviceInfoPtr CreateFakeFidoDevice() {
return device::CreateDeviceFromReportDescriptor(
/*vendor_id=*/0x1234,
/*product_id=*/0xabcd, device::TestReportDescriptors::FidoU2fHid());
}
// A mock HidManagerClient implementation that can be used to listen for HID
// device connection events.
class MockHidManagerClient : public HidChooserContext::HidManagerClient {
public:
MockHidManagerClient() = default;
MockHidManagerClient(const MockHidManagerClient&) = delete;
MockHidManagerClient& operator=(const MockHidManagerClient&) = delete;
~MockHidManagerClient() override = default;
mojo::PendingAssociatedRemote<HidManagerClient> BindReceiverAndPassRemote() {
auto client = receiver_.BindNewEndpointAndPassRemote();
receiver_.set_disconnect_handler(base::BindOnce(
&MockHidManagerClient::OnConnectionError, base::Unretained(this)));
return client;
}
MOCK_METHOD1(DeviceAdded, void(device::mojom::HidDeviceInfoPtr));
MOCK_METHOD1(DeviceRemoved, void(device::mojom::HidDeviceInfoPtr));
MOCK_METHOD1(DeviceChanged, void(device::mojom::HidDeviceInfoPtr));
MOCK_METHOD0(OnHidChooserContextShutdown, void());
MOCK_METHOD0(ConnectionError, void());
void OnConnectionError() {
receiver_.reset();
ConnectionError();
}
private:
mojo::AssociatedReceiver<HidManagerClient> receiver_{this};
};
// A fake HidConnectionClient implementation.
class FakeHidConnectionClient : public device::mojom::HidConnectionClient {
public:
FakeHidConnectionClient() = default;
FakeHidConnectionClient(FakeHidConnectionClient&) = delete;
FakeHidConnectionClient& operator=(FakeHidConnectionClient&) = delete;
~FakeHidConnectionClient() override = default;
void Bind(
mojo::PendingReceiver<device::mojom::HidConnectionClient> receiver) {
receiver_.Bind(std::move(receiver));
}
// mojom::HidConnectionClient:
void OnInputReport(uint8_t report_id,
const std::vector<uint8_t>& buffer) override {}
private:
mojo::Receiver<device::mojom::HidConnectionClient> receiver_{this};
};
class MockHidConnectionTracker : public HidConnectionTracker {
public:
explicit MockHidConnectionTracker(Profile* profile)
: HidConnectionTracker(profile) {}
~MockHidConnectionTracker() override = default;
MOCK_METHOD(void, IncrementConnectionCount, (const url::Origin&), (override));
MOCK_METHOD(void, DecrementConnectionCount, (const url::Origin&), (override));
};
class ChromeHidTestHelper {
public:
void SimulateDeviceServiceCrash() {
hid_manager_->SimulateConnectionError();
hid_manager_.reset();
base::RunLoop().RunUntilIdle();
// Re-bind a new fake HidManager so tests don't create a real one.
BindHidManager();
}
void AddDevice(const device::mojom::HidDeviceInfoPtr& device) {
hid_manager_->AddDevice(device.Clone());
}
void ChangeDevice(const device::mojom::HidDeviceInfoPtr& device) {
hid_manager_->ChangeDevice(device.Clone());
}
void RemoveDevice(const device::mojom::HidDeviceInfoPtr& device) {
hid_manager_->RemoveDevice(device->guid);
}
HidChooserContext* GetChooserContext() {
return HidChooserContextFactory::GetForProfile(profile_);
}
virtual void ConnectToService(
mojo::PendingReceiver<blink::mojom::HidService> receiver) = 0;
void BindHidManager() {
EXPECT_FALSE(hid_manager_);
hid_manager_ = std::make_unique<device::FakeHidManager>();
mojo::PendingRemote<device::mojom::HidManager> pending_remote;
hid_manager_->Bind(pending_remote.InitWithNewPipeAndPassReceiver());
TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> devices_future;
GetChooserContext()->SetHidManagerForTesting(std::move(pending_remote),
devices_future.GetCallback());
EXPECT_TRUE(devices_future.Wait());
}
#if BUILDFLAG(IS_CHROMEOS)
const user_manager::User* SetUpUserManager() {
// On ChromeOS a user account is needed in order to check whether the user
// account is affiliated with the device owner for the purposes of applying
// enterprise policy.
constexpr GaiaId::Literal kTestUserGaiaId("1111111111");
auto fake_user_manager = std::make_unique<ash::FakeChromeUserManager>();
auto* fake_user_manager_ptr = fake_user_manager.get();
scoped_user_manager_ = std::make_unique<user_manager::ScopedUserManager>(
std::move(fake_user_manager));
auto account_id =
AccountId::FromUserEmailGaiaId(kTestUserEmail, kTestUserGaiaId);
const user_manager::User* user = fake_user_manager_ptr->AddUser(account_id);
fake_user_manager_ptr->LoginUser(account_id);
return user;
}
void TearDownUserManager() { scoped_user_manager_.reset(); }
#endif // BUILDFLAG(IS_CHROMEOS)
#if BUILDFLAG(ENABLE_EXTENSIONS)
// Creates a fake extension with the specified `extension_id` so that it can
// exercise behaviors that are only enabled for privileged extensions.
scoped_refptr<const extensions::Extension> CreateExtensionWithId(
std::string_view extension_id) {
auto manifest =
base::Value::Dict()
.Set("name", "Fake extension")
.Set("description", "For testing.")
.Set("version", "0.1")
.Set("manifest_version", 2)
.Set("web_accessible_resources",
base::Value::List().Append(kExtensionDocumentFileName));
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder()
.SetManifest(std::move(manifest))
.SetID(std::string(extension_id))
.Build();
if (!extension) {
return nullptr;
}
extensions::TestExtensionSystem* extension_system =
static_cast<extensions::TestExtensionSystem*>(
extensions::ExtensionSystem::Get(profile_));
extension_system->CreateExtensionService(
base::CommandLine::ForCurrentProcess(), base::FilePath(), false);
extensions::ExtensionRegistrar::Get(profile_)->AddExtension(extension);
return extension;
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
void SetUpWebPageOriginUrl() { origin_url_ = GURL(kDefaultTestUrl); }
void SetUpExtensionOriginUrl() {
extension_ = CreateExtensionWithId(kPrivilegedExtensionId);
origin_url_ = extension_->origin().GetURL();
}
virtual void SetUpOriginUrl() = 0;
BrowserContextKeyedServiceFactory::TestingFactory
GetHidConnectionTrackerTestingFactory() {
return base::BindRepeating([](content::BrowserContext* browser_context) {
return static_cast<std::unique_ptr<KeyedService>>(
std::make_unique<testing::NiceMock<MockHidConnectionTracker>>(
Profile::FromBrowserContext(browser_context)));
});
}
void SetUpHidConnectionTracker() {
// Even MockHidConnectionTracker can be lazily created in ChromeHidDelegate,
// we intentionally create it ahead of time so that we can test EXPECT_CALL
// for invoking mock method for the first time.
hid_connection_tracker_ = static_cast<MockHidConnectionTracker*>(
HidConnectionTrackerFactory::GetForProfile(profile_, /*create=*/true));
}
MockHidConnectionTracker& hid_connection_tracker() {
return *hid_connection_tracker_;
}
MockHidManagerClient& hid_manager_client() { return hid_manager_client_; }
void TestHidServiceNotConnected() {
base::RunLoop run_loop;
mojo::Remote<blink::mojom::HidService> hid_service;
ConnectToService(hid_service.BindNewPipeAndPassReceiver());
hid_service.set_disconnect_handler(run_loop.QuitClosure());
run_loop.Run();
EXPECT_FALSE(hid_service.is_connected());
}
void TestAddChangeRemoveDevice() {
const auto origin = url::Origin::Create(origin_url_);
// Connect a device with one of its collections missing.
auto incomplete_device = CreateIncompleteFakeDevice();
AddDevice(incomplete_device);
// Grant permission to access `incomplete_device` from `origin`.
GetChooserContext()->GrantDevicePermission(origin, *incomplete_device);
// Create the HidService and register a mock client to receive
// notifications on device connections and disconnections.
mojo::Remote<blink::mojom::HidService> hid_service;
ConnectToService(hid_service.BindNewPipeAndPassReceiver());
hid_service->RegisterClient(
hid_manager_client().BindReceiverAndPassRemote());
// Call GetDevices to ensure the service is started and the client is set.
{
TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> devices_future;
hid_service->GetDevices(devices_future.GetCallback());
ASSERT_THAT(devices_future.Get(),
ElementsAre(HasGuid(incomplete_device->guid)));
EXPECT_EQ(devices_future.Get().front()->collections.size(),
incomplete_device->collections.size());
}
// Update the device with the missing collection.
auto complete_device = CreateFakeDevice();
complete_device->guid = incomplete_device->guid;
complete_device->serial_number = incomplete_device->serial_number;
TestFuture<device::mojom::HidDeviceInfoPtr> device_changed_future;
EXPECT_CALL(hid_manager_client(), DeviceChanged).WillOnce([&](auto d) {
device_changed_future.SetValue(std::move(d));
});
ChangeDevice(complete_device);
EXPECT_EQ(device_changed_future.Get()->guid, complete_device->guid);
// Call GetDevices and make sure there is still only one device with the
// same `guid` but the complete device info is returned.
{
TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> devices_future;
hid_service->GetDevices(devices_future.GetCallback());
ASSERT_THAT(devices_future.Get(),
ElementsAre(HasGuid(incomplete_device->guid)));
EXPECT_EQ(devices_future.Get().front()->collections.size(),
complete_device->collections.size());
}
// Disconnect the devices. The mock client should be notified.
TestFuture<device::mojom::HidDeviceInfoPtr> device_removed_future;
EXPECT_CALL(hid_manager_client(), DeviceRemoved).WillOnce([&](auto d) {
device_removed_future.SetValue(std::move(d));
});
RemoveDevice(incomplete_device);
EXPECT_EQ(device_removed_future.Get()->guid, incomplete_device->guid);
// Reconnect the device. The mock client should be notified.
TestFuture<device::mojom::HidDeviceInfoPtr> device_added_future;
EXPECT_CALL(hid_manager_client(), DeviceAdded).WillOnce([&](auto d) {
device_added_future.SetValue(std::move(d));
});
AddDevice(complete_device);
EXPECT_EQ(device_added_future.Get()->guid, incomplete_device->guid);
}
void TestNoPermissionDevice() {
const auto origin = url::Origin::Create(origin_url_);
// Connect two devices.
auto allowed_device1 = CreateFakeDevice();
AddDevice(allowed_device1);
auto other_device1 = CreateFakeDevice();
AddDevice(other_device1);
// Grant permission to access `allowed_device1` from `origin`.
GetChooserContext()->GrantDevicePermission(origin, *allowed_device1);
// Create the HidService and register a mock client to receive
// notifications on device connections and disconnections.
mojo::Remote<blink::mojom::HidService> hid_service;
ConnectToService(hid_service.BindNewPipeAndPassReceiver());
hid_service->RegisterClient(
hid_manager_client().BindReceiverAndPassRemote());
// Call GetDevices to ensure the service is started and the client is set.
TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> devices_future;
hid_service->GetDevices(devices_future.GetCallback());
EXPECT_THAT(devices_future.Get(),
ElementsAre(HasGuid(allowed_device1->guid)));
// Connect two more devices.
auto allowed_device2 = CreateFakeDevice();
AddDevice(allowed_device2);
auto other_device2 = CreateFakeDevice();
AddDevice(other_device2);
// Grant permission to access `allowed_device2` from `origin`.
GetChooserContext()->GrantDevicePermission(origin, *allowed_device2);
// Disconnect all four devices. The mock client should be notified only
// for the devices it has permission to access.
TestFuture<device::mojom::HidDeviceInfoPtr> device_removed_future;
EXPECT_CALL(hid_manager_client(), DeviceRemoved)
.Times(2)
.WillRepeatedly(
[&](auto d) { device_removed_future.SetValue(std::move(d)); });
RemoveDevice(allowed_device1);
RemoveDevice(allowed_device2);
RemoveDevice(other_device1);
RemoveDevice(other_device2);
EXPECT_EQ(device_removed_future.Take()->guid, allowed_device1->guid);
EXPECT_EQ(device_removed_future.Take()->guid, allowed_device2->guid);
// Reconnect all four devices. The mock client should be notified only for
// the devices it has permission to access.
TestFuture<device::mojom::HidDeviceInfoPtr> device_added_future;
EXPECT_CALL(hid_manager_client(), DeviceAdded)
.Times(2)
.WillRepeatedly(
[&](auto d) { device_added_future.SetValue(std::move(d)); });
AddDevice(allowed_device1);
AddDevice(allowed_device2);
AddDevice(other_device1);
AddDevice(other_device2);
EXPECT_EQ(device_added_future.Take()->guid, allowed_device1->guid);
EXPECT_EQ(device_added_future.Take()->guid, allowed_device2->guid);
}
void TestReconnectHidService() {
const auto origin = url::Origin::Create(origin_url_);
// Connect two devices. Configure `ephemeral_device` with no serial number
// so it is not eligible for persistent permissions.
auto device = CreateFakeDevice();
auto ephemeral_device = CreateFakeDevice();
ephemeral_device->serial_number = "";
AddDevice(device);
AddDevice(ephemeral_device);
// Grant permission for `origin` to access both devices. `device` is
// eligible for persistent permissions and `ephemeral_device` is only
// eligible for ephemeral permissions.
GetChooserContext()->GrantDevicePermission(origin, *device);
GetChooserContext()->GrantDevicePermission(origin, *ephemeral_device);
// Create the HidService and register a mock client to receive
// notifications on device connections and disconnections. Call `GetDevices`
// to ensure the service is started and the client is set.
mojo::Remote<blink::mojom::HidService> hid_service;
ConnectToService(hid_service.BindNewPipeAndPassReceiver());
hid_service->RegisterClient(
hid_manager_client().BindReceiverAndPassRemote());
{
TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> devices_future;
hid_service->GetDevices(devices_future.GetCallback());
EXPECT_THAT(devices_future.Get(),
UnorderedElementsAre(HasGuid(device->guid),
HasGuid(ephemeral_device->guid)));
}
// Both permissions are granted.
EXPECT_TRUE(GetChooserContext()->HasDevicePermission(origin, *device));
EXPECT_TRUE(
GetChooserContext()->HasDevicePermission(origin, *ephemeral_device));
// Simulate a device service crash.
base::RunLoop loop;
EXPECT_CALL(hid_manager_client(), ConnectionError).WillOnce([&]() {
loop.Quit();
});
SimulateDeviceServiceCrash();
loop.Run();
// The ephemeral permission is revoked.
EXPECT_TRUE(GetChooserContext()->HasDevicePermission(origin, *device));
EXPECT_FALSE(
GetChooserContext()->HasDevicePermission(origin, *ephemeral_device));
// Add a new device eligible for persistent permissions.
auto another_device = CreateFakeDevice();
AddDevice(another_device);
EXPECT_CALL(hid_manager_client(), DeviceAdded).Times(0);
base::RunLoop().RunUntilIdle();
// Grant the device permission while the service is off.
GetChooserContext()->GrantDevicePermission(origin, *another_device);
// `mock_client` is not notified when `device` is removed because the
// service is off.
RemoveDevice(device);
EXPECT_CALL(hid_manager_client(), DeviceRemoved).Times(0);
base::RunLoop().RunUntilIdle();
// Reconnect the service.
hid_service.reset();
testing::Mock::VerifyAndClearExpectations(&hid_manager_client());
ConnectToService(hid_service.BindNewPipeAndPassReceiver());
hid_service->RegisterClient(
hid_manager_client().BindReceiverAndPassRemote());
{
TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> devices_future;
hid_service->GetDevices(devices_future.GetCallback());
EXPECT_THAT(devices_future.Get(),
ElementsAre(HasGuid(another_device->guid)));
}
// The persistent permissions are still granted.
EXPECT_TRUE(GetChooserContext()->HasDevicePermission(origin, *device));
EXPECT_TRUE(
GetChooserContext()->HasDevicePermission(origin, *another_device));
EXPECT_FALSE(
GetChooserContext()->HasDevicePermission(origin, *ephemeral_device));
}
void TestRevokeDevicePermission() {
const auto origin = url::Origin::Create(origin_url_);
// Connect a device.
auto device = CreateFakeDevice();
AddDevice(device);
// Create the `HidService`.
mojo::Remote<blink::mojom::HidService> hid_service;
ConnectToService(hid_service.BindNewPipeAndPassReceiver());
// Call GetDevices to ensure the service is started.
TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> devices_future;
hid_service->GetDevices(devices_future.GetCallback());
EXPECT_TRUE(devices_future.Get().empty());
// Grant permission to access the connected device.
GetChooserContext()->GrantDevicePermission(origin, *device);
auto objects = GetChooserContext()->GetGrantedObjects(origin);
ASSERT_EQ(1u, objects.size());
// Open a connection to `device`.
FakeHidConnectionClient connection_client;
mojo::PendingRemote<device::mojom::HidConnectionClient>
hid_connection_client;
connection_client.Bind(
hid_connection_client.InitWithNewPipeAndPassReceiver());
TestFuture<mojo::PendingRemote<device::mojom::HidConnection>>
pending_remote_future;
if (supports_hid_connection_tracker_) {
EXPECT_CALL(hid_connection_tracker(), IncrementConnectionCount(origin));
}
hid_service->Connect(device->guid, std::move(hid_connection_client),
pending_remote_future.GetCallback());
mojo::Remote<device::mojom::HidConnection> connection;
connection.Bind(pending_remote_future.Take());
ASSERT_TRUE(connection);
// Revoke the permission. The device should be disconnected.
base::RunLoop disconnect_loop;
connection.set_disconnect_handler(disconnect_loop.QuitClosure());
base::RunLoop decrement_connection_count_loop;
if (supports_hid_connection_tracker_) {
EXPECT_CALL(hid_connection_tracker(), DecrementConnectionCount(origin))
.WillOnce(RunClosure(decrement_connection_count_loop.QuitClosure()));
}
GetChooserContext()->RevokeDevicePermission(origin, *device);
disconnect_loop.Run();
if (supports_hid_connection_tracker_) {
decrement_connection_count_loop.Run();
}
}
void TestRevokeDevicePermissionEphemeral() {
const auto origin = url::Origin::Create(origin_url_);
// Connect a device. Configure it with no serial number so it is not
// eligible for persistent permissions.
auto device = CreateFakeDevice();
device->serial_number = "";
AddDevice(device);
// Create the `HidService`.
mojo::Remote<blink::mojom::HidService> hid_service;
ConnectToService(hid_service.BindNewPipeAndPassReceiver());
// Call GetDevices to ensure the service is started.
TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> devices_future;
hid_service->GetDevices(devices_future.GetCallback());
EXPECT_TRUE(devices_future.Get().empty());
// Grant permission to access the connected device.
GetChooserContext()->GrantDevicePermission(origin, *device);
auto objects = GetChooserContext()->GetGrantedObjects(origin);
ASSERT_EQ(1u, objects.size());
// Open a connection to `device`.
FakeHidConnectionClient connection_client;
mojo::PendingRemote<device::mojom::HidConnectionClient>
hid_connection_client;
connection_client.Bind(
hid_connection_client.InitWithNewPipeAndPassReceiver());
TestFuture<mojo::PendingRemote<device::mojom::HidConnection>>
pending_remote_future;
if (supports_hid_connection_tracker_) {
EXPECT_CALL(hid_connection_tracker(), IncrementConnectionCount(origin));
}
hid_service->Connect(device->guid, std::move(hid_connection_client),
pending_remote_future.GetCallback());
mojo::Remote<device::mojom::HidConnection> connection;
connection.Bind(pending_remote_future.Take());
ASSERT_TRUE(connection);
// Revoke the permission. The device should be disconnected.
base::RunLoop disconnect_loop;
connection.set_disconnect_handler(disconnect_loop.QuitClosure());
base::RunLoop decrement_connection_count_loop;
if (supports_hid_connection_tracker_) {
EXPECT_CALL(hid_connection_tracker(), DecrementConnectionCount(origin))
.WillOnce(RunClosure(decrement_connection_count_loop.QuitClosure()));
}
GetChooserContext()->RevokeDevicePermission(origin, *device);
disconnect_loop.Run();
if (supports_hid_connection_tracker_) {
decrement_connection_count_loop.Run();
}
}
void TestConnectAndDisconnect(content::WebContents* web_contents) {
const auto origin = url::Origin::Create(origin_url_);
// Create the `HidService`.
mojo::Remote<blink::mojom::HidService> hid_service;
ConnectToService(hid_service.BindNewPipeAndPassReceiver());
// Connect a device.
auto device = CreateFakeDevice();
AddDevice(device);
// Grant permission to access `device` from `origin`.
GetChooserContext()->GrantDevicePermission(origin, *device);
// Call `GetDevices` and expect the device to be returned.
TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> devices_future;
hid_service->GetDevices(devices_future.GetCallback());
EXPECT_THAT(devices_future.Take(), ElementsAre(HasGuid(device->guid)));
if (web_contents) {
// The `WebContents` should not indicate we are connected to a device.
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kHID));
}
// Open a connection to `device`.
FakeHidConnectionClient connection_client;
mojo::PendingRemote<device::mojom::HidConnectionClient>
hid_connection_client;
connection_client.Bind(
hid_connection_client.InitWithNewPipeAndPassReceiver());
TestFuture<mojo::PendingRemote<device::mojom::HidConnection>>
pending_remote_future;
if (supports_hid_connection_tracker_) {
EXPECT_CALL(hid_connection_tracker(), IncrementConnectionCount(origin));
}
hid_service->Connect(device->guid, std::move(hid_connection_client),
pending_remote_future.GetCallback());
mojo::Remote<device::mojom::HidConnection> connection;
connection.Bind(pending_remote_future.Take());
ASSERT_TRUE(connection);
if (web_contents) {
// Now the `WebContents` should indicate we are connected to a device.
EXPECT_TRUE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kHID));
}
// Close `connection` and check that the `WebContents` no longer indicates
// we are connected.
// If the scenario supports HidConnectionTracker, we can avoid using
// RunUntilIdle() by using HidConnectionTracker::DecrementConnectionCount()
// as it will be called in the disconnect path.
if (supports_hid_connection_tracker_) {
base::RunLoop decrement_connection_count_loop;
EXPECT_CALL(hid_connection_tracker(), DecrementConnectionCount(origin))
.WillOnce(RunClosure(decrement_connection_count_loop.QuitClosure()));
connection.reset();
decrement_connection_count_loop.Run();
} else {
connection.reset();
base::RunLoop().RunUntilIdle();
}
if (web_contents) {
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kHID));
}
}
void TestConnectAndRemove(content::WebContents* web_contents) {
const auto origin = url::Origin::Create(origin_url_);
// Create the `HidService`.
mojo::Remote<blink::mojom::HidService> hid_service;
ConnectToService(hid_service.BindNewPipeAndPassReceiver());
hid_service->RegisterClient(
hid_manager_client().BindReceiverAndPassRemote());
// Connect a device.
auto device = CreateFakeDevice();
AddDevice(device);
// Grant permission to access `device` from `origin`.
GetChooserContext()->GrantDevicePermission(origin, *device);
// Call `GetDevices` and expect the device to be returned.
TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> devices_future;
hid_service->GetDevices(devices_future.GetCallback());
EXPECT_THAT(devices_future.Take(), ElementsAre(HasGuid(device->guid)));
if (web_contents) {
// The `WebContents` should not indicate we are connected to a device.
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kHID));
}
// Open a connection to `device`.
FakeHidConnectionClient connection_client;
mojo::PendingRemote<device::mojom::HidConnectionClient>
hid_connection_client;
connection_client.Bind(
hid_connection_client.InitWithNewPipeAndPassReceiver());
TestFuture<mojo::PendingRemote<device::mojom::HidConnection>>
pending_remote_future;
if (supports_hid_connection_tracker_) {
EXPECT_CALL(hid_connection_tracker(), IncrementConnectionCount(origin));
}
hid_service->Connect(device->guid, std::move(hid_connection_client),
pending_remote_future.GetCallback());
mojo::Remote<device::mojom::HidConnection> connection;
connection.Bind(pending_remote_future.Take());
ASSERT_TRUE(connection);
if (web_contents) {
// Now the `WebContents` should indicate we are connected to a device.
EXPECT_TRUE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kHID));
}
// Remove `device` and check that the `WebContents` no longer indicates we
// are connected.
// If the scenario supports HidConnectionTracker, we can avoid using
// RunUntilIdle() by using HidConnectionTracker::DecrementConnectionCount()
// as it will be called in the remove device path.
if (supports_hid_connection_tracker_) {
base::RunLoop decrement_connection_count_loop;
EXPECT_CALL(hid_connection_tracker(), DecrementConnectionCount(origin))
.WillOnce(RunClosure(decrement_connection_count_loop.QuitClosure()));
RemoveDevice(device);
decrement_connection_count_loop.Run();
} else {
RemoveDevice(device);
base::RunLoop().RunUntilIdle();
}
if (web_contents) {
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kHID));
}
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
void TestFidoDeviceAllowedWithPrivilegedOrigin(
content::WebContents* web_contents) {
auto origin = url::Origin::Create(origin_url_);
// Connect a FIDO device.
auto device = CreateFakeFidoDevice();
AddDevice(device);
// Grant permission to access `device` from `privileged_origin`.
GetChooserContext()->GrantDevicePermission(origin, *device);
// Create the `HidService`.
mojo::Remote<blink::mojom::HidService> hid_service;
ConnectToService(hid_service.BindNewPipeAndPassReceiver());
// Call `GetDevices` and expect the device to be returned.
TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> devices_future;
hid_service->GetDevices(devices_future.GetCallback());
EXPECT_THAT(devices_future.Take(), ElementsAre(HasGuid(device->guid)));
if (web_contents) {
// The `WebContents` should not indicate we are connected to a device.
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kHID));
}
// Open a connection to `device`.
FakeHidConnectionClient connection_client;
mojo::PendingRemote<device::mojom::HidConnectionClient>
hid_connection_client;
connection_client.Bind(
hid_connection_client.InitWithNewPipeAndPassReceiver());
TestFuture<mojo::PendingRemote<device::mojom::HidConnection>>
pending_remote_future;
hid_service->Connect(device->guid, std::move(hid_connection_client),
pending_remote_future.GetCallback());
mojo::Remote<device::mojom::HidConnection> connection;
connection.Bind(pending_remote_future.Take());
ASSERT_TRUE(connection);
if (web_contents) {
// Now the `WebContents` should indicate we are connected to a device.
EXPECT_TRUE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kHID));
}
}
#endif
void TestConnectionTrackerOpenDeviceNoConnectionCountUpdate() {
mojo::Remote<blink::mojom::HidService> hid_service;
ConnectToService(hid_service.BindNewPipeAndPassReceiver());
auto origin = url::Origin::Create(origin_url_);
auto device = CreateFakeDevice();
AddDevice(device);
GetChooserContext()->GrantDevicePermission(origin, *device);
// Call |GetDevices| and expect the device to be returned.
TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> devices_future;
hid_service->GetDevices(devices_future.GetCallback());
EXPECT_THAT(devices_future.Take(), ElementsAre(HasGuid(device->guid)));
EXPECT_CALL(hid_connection_tracker(), IncrementConnectionCount(origin))
.Times(0);
// Open a connection to `device`.
FakeHidConnectionClient connection_client;
mojo::PendingRemote<device::mojom::HidConnectionClient>
hid_connection_client;
connection_client.Bind(
hid_connection_client.InitWithNewPipeAndPassReceiver());
TestFuture<mojo::PendingRemote<device::mojom::HidConnection>>
pending_remote_future;
hid_service->Connect(device->guid, std::move(hid_connection_client),
pending_remote_future.GetCallback());
mojo::Remote<device::mojom::HidConnection> connection;
connection.Bind(pending_remote_future.Take());
ASSERT_TRUE(connection);
}
protected:
raw_ptr<TestingProfile, DanglingUntriaged> profile_ = nullptr;
GURL origin_url_;
raw_ptr<MockHidConnectionTracker, DanglingUntriaged> hid_connection_tracker_ =
nullptr;
// This flag is expected to be set to true only for the scenario of extension
// origin and kEnableWebHidOnExtensionServiceWorker enabled.
bool supports_hid_connection_tracker_ = false;
private:
std::unique_ptr<device::FakeHidManager> hid_manager_;
#if BUILDFLAG(IS_CHROMEOS)
std::unique_ptr<user_manager::ScopedUserManager> scoped_user_manager_;
#endif
scoped_refptr<const extensions::Extension> extension_;
MockHidManagerClient hid_manager_client_;
};
class ChromeHidDelegateRenderFrameTestBase
: public ChromeRenderViewHostTestHarness,
public ChromeHidTestHelper {
public:
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
#if BUILDFLAG(IS_CHROMEOS)
const user_manager::User* user = SetUpUserManager();
TestingProfile::Builder builder;
testing_profile_ = builder.Build();
profile_ = testing_profile_.get();
ash::ProfileHelper::Get()->SetUserToProfileMappingForTesting(
user, profile_.get());
#else
profile_manager_ = std::make_unique<TestingProfileManager>(
TestingBrowserProcess::GetGlobal());
ASSERT_TRUE(profile_manager_->SetUp());
// TODO(crbug.com/40249783): Pass testing factory when creating profile.
// Ideally, we should be able to pass testing factory when calling profile
// manager's CreateTestingProfile. However, due to the fact that:
// 1) TestingProfile::TestingProfile(...) will call BrowserContextShutdown
// as part of setting testing factory.
// 2) HidConnectionTrackerFactory::BrowserContextShutdown() at some point
// need valid profile_metrics::GetBrowserProfileType() as part of
// HidConnectionTrackerFactory::GetForProfile().
// It will hit failure in profile_metrics::GetBrowserProfileType() because
// the profile is not initialized properly before setting testing factory.
// As a result, here create a profile then call SetTestingFactory to inject
// MockHidConnectionTracker.
profile_ = profile_manager_->CreateTestingProfile(kTestUserEmail);
#endif
HidConnectionTrackerFactory::GetInstance()->SetTestingFactory(
profile_, GetHidConnectionTrackerTestingFactory());
ASSERT_TRUE(profile_);
SetUpHidConnectionTracker();
// Create a new web contents for `profile_`.
SetContents(
content::WebContentsTester::CreateTestWebContents(profile_, nullptr));
BindHidManager();
SetUpOriginUrl();
NavigateAndCommit(origin_url_);
}
void TearDown() override {
DeleteContents();
#if BUILDFLAG(IS_CHROMEOS)
testing_profile_.reset();
TearDownUserManager();
#else
profile_manager_->DeleteAllTestingProfiles();
profile_manager_.reset();
#endif
profile_ = nullptr;
ChromeRenderViewHostTestHarness::TearDown();
hid_connection_tracker_ = nullptr;
}
// ChromeHidTestHelper
void ConnectToService(
mojo::PendingReceiver<blink::mojom::HidService> receiver) override {
content::RenderFrameHostTester::For(main_rfh())
->CreateHidServiceForTesting(std::move(receiver));
}
void TestConnectAndNavigateCrossDocument(content::WebContents* web_contents) {
// The test assumes the previous page gets deleted after navigation,
// disconnecting the device. Disable back/forward cache to ensure that it
// doesn't get preserved in the cache.
// TODO(crbug.com/40232335): Integrate WebHID with bfcache and remove this.
content::DisableBackForwardCacheForTesting(
web_contents, content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
const auto origin = url::Origin::Create(origin_url_);
// Create the `HidService`.
mojo::Remote<blink::mojom::HidService> hid_service;
ConnectToService(hid_service.BindNewPipeAndPassReceiver());
// Connect a device.
auto device = CreateFakeDevice();
AddDevice(device);
// Grant permission to access `device` from `origin`.
GetChooserContext()->GrantDevicePermission(origin, *device);
// Call `GetDevices` and expect the device to be returned.
TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> devices_future;
hid_service->GetDevices(devices_future.GetCallback());
EXPECT_THAT(devices_future.Take(), ElementsAre(HasGuid(device->guid)));
// The `WebContents` should not indicate we are connected to a device.
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kHID));
// Open a connection to `device`.
FakeHidConnectionClient connection_client;
mojo::PendingRemote<device::mojom::HidConnectionClient>
hid_connection_client;
connection_client.Bind(
hid_connection_client.InitWithNewPipeAndPassReceiver());
TestFuture<mojo::PendingRemote<device::mojom::HidConnection>>
pending_remote_future;
hid_service->Connect(device->guid, std::move(hid_connection_client),
pending_remote_future.GetCallback());
mojo::Remote<device::mojom::HidConnection> connection;
connection.Bind(pending_remote_future.Take());
ASSERT_TRUE(connection);
// Now the `WebContents` should indicate we are connected to a device.
EXPECT_TRUE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kHID));
// Perform a cross-document navigation. The `WebContents` should no longer
// indicate we are connected.
NavigateAndCommit(GURL(kCrossOriginTestUrl));
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kHID));
}
private:
#if BUILDFLAG(IS_CHROMEOS)
std::unique_ptr<TestingProfile> testing_profile_;
#else
std::unique_ptr<TestingProfileManager> profile_manager_;
#endif
};
class ChromeHidDelegateRenderFrameTest
: public ChromeHidDelegateRenderFrameTestBase {
// ChromeHidTestHelper
void SetUpOriginUrl() override { SetUpWebPageOriginUrl(); }
};
class ChromeHidDelegateServiceWorkerTestBase
: public content::EmbeddedWorkerInstanceTestHarness,
public ChromeHidTestHelper {
public:
void SetUp() override {
content::EmbeddedWorkerInstanceTestHarness::SetUp();
#if BUILDFLAG(IS_CHROMEOS)
SetUpUserManager();
#endif
SetUpHidConnectionTracker();
BindHidManager();
SetUpOriginUrl();
StartWorker();
}
void TearDown() override {
StopWorker();
#if BUILDFLAG(IS_CHROMEOS)
TearDownUserManager();
#endif
content::EmbeddedWorkerInstanceTestHarness::TearDown();
}
// ChromeHidTestHelper
void ConnectToService(
mojo::PendingReceiver<blink::mojom::HidService> receiver) override {
BindHidServiceToWorker(origin_url_, std::move(receiver));
}
void StartWorker() {
auto worker_url =
GURL(base::StringPrintf("%s/worker.js", origin_url_.spec().c_str()));
CreateAndStartWorker(origin_url_, worker_url);
// Wait until tasks triggered by ServiceWorkerHidDelegateObserver settle.
base::RunLoop().RunUntilIdle();
}
void StopWorker() { StopAndResetWorker(); }
// content::EmbeddedWorkerInstanceTestHarness
std::unique_ptr<content::BrowserContext> CreateBrowserContext() override {
auto builder = TestingProfile::Builder();
auto testing_profile = builder.Build();
profile_ = testing_profile.get();
// TODO(crbug.com/40249783): Pass testing factory when creating profile.
// Ideally, we should use TestingProfile::Builder::AddTestingFactory to
// inject MockHidConnectionTracker. However, due to the fact that:
// 1) TestingProfile::TestingProfile(...) will call BrowserContextShutdown
// as part of setting testing factory.
// 2) HidConnectionTrackerFactory::BrowserContextShutdown() at some point
// need valid profile_metrics::GetBrowserProfileType() as part of
// HidConnectionTrackerFactory::GetForProfile().
// It will hit failure in profile_metrics::GetBrowserProfileType() due to
// profile is not initialized properly before setting testing factory. As a
// result, here create a profile then call SetTestingFactory to inject
// MockHidConnectionTracker.
HidConnectionTrackerFactory::GetInstance()->SetTestingFactory(
profile_, GetHidConnectionTrackerTestingFactory());
return testing_profile;
}
private:
ScopedTestingLocalState testing_local_state_{
TestingBrowserProcess::GetGlobal()};
};
class ChromeHidDelegateServiceWorkerTest
: public ChromeHidDelegateServiceWorkerTestBase {
public:
// ChromeHidTestHelper
void SetUpOriginUrl() override { SetUpWebPageOriginUrl(); }
};
#if BUILDFLAG(ENABLE_EXTENSIONS)
class ChromeHidDelegateExtensionServiceWorkerTest
: public ChromeHidDelegateServiceWorkerTestBase {
public:
ChromeHidDelegateExtensionServiceWorkerTest() {
supports_hid_connection_tracker_ = true;
}
// ChromeHidTestHelper
void SetUpOriginUrl() override { SetUpExtensionOriginUrl(); }
};
class ChromeHidDelegateExtensionRenderFrameTest
: public ChromeHidDelegateRenderFrameTestBase {
public:
ChromeHidDelegateExtensionRenderFrameTest() {
supports_hid_connection_tracker_ = true;
}
// ChromeHidTestHelper
void SetUpOriginUrl() override { SetUpExtensionOriginUrl(); }
};
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
} // namespace
TEST_F(ChromeHidDelegateRenderFrameTest, AddChangeRemoveDevice) {
TestAddChangeRemoveDevice();
}
TEST_F(ChromeHidDelegateRenderFrameTest, NoPermissionDevice) {
TestNoPermissionDevice();
}
TEST_F(ChromeHidDelegateRenderFrameTest, ReconnectHidService) {
TestReconnectHidService();
}
TEST_F(ChromeHidDelegateRenderFrameTest, RevokeDevicePermission) {
TestRevokeDevicePermission();
}
TEST_F(ChromeHidDelegateRenderFrameTest, RevokeDevicePermissionEphemeral) {
TestRevokeDevicePermissionEphemeral();
;
}
TEST_F(ChromeHidDelegateRenderFrameTest, ConnectAndDisconnect) {
TestConnectAndDisconnect(web_contents());
}
TEST_F(ChromeHidDelegateRenderFrameTest, ConnectAndRemove) {
TestConnectAndRemove(web_contents());
}
TEST_F(ChromeHidDelegateRenderFrameTest, ConnectAndNavigateCrossDocument) {
TestConnectAndNavigateCrossDocument(web_contents());
}
TEST_F(ChromeHidDelegateServiceWorkerTest, HidServiceNotConnected) {
TestHidServiceNotConnected();
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
TEST_F(ChromeHidDelegateExtensionRenderFrameTest,
FidoDeviceAllowedWithPrivilegedOrigin) {
TestFidoDeviceAllowedWithPrivilegedOrigin(web_contents());
}
TEST_F(ChromeHidDelegateExtensionRenderFrameTest, AddChangeRemoveDevice) {
TestAddChangeRemoveDevice();
}
TEST_F(ChromeHidDelegateExtensionRenderFrameTest, NoPermissionDevice) {
TestNoPermissionDevice();
}
TEST_F(ChromeHidDelegateExtensionRenderFrameTest, ReconnectHidService) {
TestReconnectHidService();
}
TEST_F(ChromeHidDelegateExtensionRenderFrameTest, RevokeDevicePermission) {
TestRevokeDevicePermission();
}
TEST_F(ChromeHidDelegateExtensionRenderFrameTest,
RevokeDevicePermissionEphemeral) {
TestRevokeDevicePermissionEphemeral();
}
TEST_F(ChromeHidDelegateExtensionRenderFrameTest, ConnectAndDisconnect) {
TestConnectAndDisconnect(web_contents());
}
TEST_F(ChromeHidDelegateExtensionRenderFrameTest, ConnectAndRemove) {
TestConnectAndRemove(web_contents());
}
TEST_F(ChromeHidDelegateExtensionRenderFrameTest,
ConnectAndNavigateCrossDocument) {
TestConnectAndNavigateCrossDocument(web_contents());
}
TEST_F(ChromeHidDelegateExtensionServiceWorkerTest, AddChangeRemoveDevice) {
TestAddChangeRemoveDevice();
}
TEST_F(ChromeHidDelegateExtensionServiceWorkerTest, NoPermissionDevice) {
TestNoPermissionDevice();
}
TEST_F(ChromeHidDelegateExtensionServiceWorkerTest, ReconnectHidService) {
TestReconnectHidService();
}
TEST_F(ChromeHidDelegateExtensionServiceWorkerTest, RevokeDevicePermission) {
TestRevokeDevicePermission();
}
TEST_F(ChromeHidDelegateExtensionServiceWorkerTest,
RevokeDevicePermissionEphemeral) {
TestRevokeDevicePermissionEphemeral();
}
TEST_F(ChromeHidDelegateExtensionServiceWorkerTest, ConnectAndDisconnect) {
TestConnectAndDisconnect(/*web_contents=*/nullptr);
}
TEST_F(ChromeHidDelegateExtensionServiceWorkerTest, ConnectAndRemove) {
TestConnectAndRemove(/*web_contents=*/nullptr);
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
TEST(ChromeHidDelegateBrowserContextTest, BrowserContextIsNull) {
ChromeHidDelegate chrome_hid_delegate;
url::Origin origin = url::Origin::Create(GURL(kDefaultTestUrl));
EXPECT_FALSE(chrome_hid_delegate.CanRequestDevicePermission(
/*browser_context=*/nullptr, origin));
EXPECT_FALSE(chrome_hid_delegate.HasDevicePermission(
/*browser_context=*/nullptr, /*render_frame_host=*/nullptr, origin,
device::mojom::HidDeviceInfo()));
EXPECT_EQ(nullptr,
chrome_hid_delegate.GetHidManager(/*browser_context=*/nullptr));
EXPECT_EQ(nullptr, chrome_hid_delegate.GetDeviceInfo(
/*browser_context=*/nullptr,
base::Uuid::GenerateRandomV4().AsLowercaseString()));
EXPECT_FALSE(chrome_hid_delegate.IsFidoAllowedForOrigin(
/*browser_context=*/nullptr, origin));
}