| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/run_loop.h" |
| #include "base/test/bind.h" |
| #include "build/build_config.h" |
| #include "mojo/public/cpp/bindings/associated_receiver.h" |
| #include "mojo/public/cpp/bindings/pending_associated_receiver.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "services/device/device_service_test_base.h" |
| #include "services/device/hid/hid_manager_impl.h" |
| #include "services/device/hid/mock_hid_connection.h" |
| #include "services/device/hid/mock_hid_service.h" |
| #include "services/device/public/mojom/hid.mojom.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace device { |
| |
| namespace { |
| |
| using ::testing::ElementsAreArray; |
| |
| #if defined(OS_MAC) |
| const uint64_t kTestDeviceIds[] = {0, 1, 2, 3}; |
| #elif defined(OS_WIN) |
| const wchar_t* const kTestDeviceIds[] = {L"0", L"1", L"2", L"3"}; |
| #else |
| const char* const kTestDeviceIds[] = {"0", "1", "2", "3"}; |
| #endif |
| |
| class MockHidManagerClient : public mojom::HidManagerClient { |
| public: |
| MockHidManagerClient() = default; |
| MockHidManagerClient(const MockHidManagerClient&) = delete; |
| MockHidManagerClient& operator=(const MockHidManagerClient&) = delete; |
| ~MockHidManagerClient() override = default; |
| |
| void Bind(mojo::PendingAssociatedReceiver<mojom::HidManagerClient> receiver) { |
| receiver_.Bind(std::move(receiver)); |
| } |
| |
| MOCK_METHOD1(DeviceAdded, void(mojom::HidDeviceInfoPtr device_info)); |
| MOCK_METHOD1(DeviceRemoved, void(mojom::HidDeviceInfoPtr device_info)); |
| MOCK_METHOD1(DeviceChanged, void(mojom::HidDeviceInfoPtr device_info)); |
| |
| void SetConnection(mojo::PendingRemote<mojom::HidConnection> connection) { |
| hid_connection_.Bind(std::move(connection)); |
| } |
| |
| mojom::HidConnection* GetConnection() { return hid_connection_.get(); } |
| |
| private: |
| mojo::AssociatedReceiver<mojom::HidManagerClient> receiver_{this}; |
| mojo::Remote<mojom::HidConnection> hid_connection_; |
| }; |
| |
| class HidManagerTest : public DeviceServiceTestBase { |
| public: |
| HidManagerTest() = default; |
| HidManagerTest(const HidManagerTest&) = delete; |
| HidManagerTest& operator=(const HidManagerTest&) = delete; |
| |
| void SetUp() override { |
| DeviceServiceTestBase::SetUp(); |
| auto mock_hid_service = std::make_unique<MockHidService>(); |
| mock_hid_service_ = mock_hid_service.get(); |
| // Transfer the ownership of the |mock_hid_service| to HidManagerImpl. |
| // It is safe to use the |mock_hid_service_| in this test. |
| HidManagerImpl::SetHidServiceForTesting(std::move(mock_hid_service)); |
| device_service()->BindHidManager(hid_manager_.BindNewPipeAndPassReceiver()); |
| } |
| |
| void TearDown() override { HidManagerImpl::SetHidServiceForTesting(nullptr); } |
| |
| scoped_refptr<HidDeviceInfo> AddTestDevice0() { |
| // Construct a minimal HidDeviceInfo. |
| auto device_info = base::MakeRefCounted<HidDeviceInfo>( |
| kTestDeviceIds[0], "physical id 0", /*vendor_id=*/0, /*product_id=*/0, |
| "Hid Service Unit Test", "HidDevice-0", |
| mojom::HidBusType::kHIDBusTypeUSB, |
| /*report_descriptor=*/std::vector<uint8_t>()); |
| mock_hid_service_->AddDevice(device_info); |
| return device_info; |
| } |
| |
| scoped_refptr<HidDeviceInfo> AddTestDevice1() { |
| // Construct a minimal HidDeviceInfo with a different device ID than above. |
| auto device_info = base::MakeRefCounted<HidDeviceInfo>( |
| kTestDeviceIds[1], "physical id 1", /*vendor_id=*/0, /*product_id=*/0, |
| "Hid Service Unit Test", "HidDevice-1", |
| mojom::HidBusType::kHIDBusTypeUSB, |
| /*report_descriptor=*/std::vector<uint8_t>()); |
| mock_hid_service_->AddDevice(device_info); |
| return device_info; |
| } |
| |
| scoped_refptr<HidDeviceInfo> AddTestDeviceWithTopLevelCollection() { |
| // Construct a HidDeviceInfo with a top-level collection. The collection has |
| // a usage ID from the FIDO usage page. |
| auto collection_info = mojom::HidCollectionInfo::New(); |
| collection_info->usage = mojom::HidUsageAndPage::New(1, 0xf1d0); |
| auto device_info = base::MakeRefCounted<HidDeviceInfo>( |
| kTestDeviceIds[2], "physical id 2", "interface id 2", /*vendor_id=*/0, |
| /*product_id=*/0, "Hid Service Unit Test", "HidDevice-2", |
| mojom::HidBusType::kHIDBusTypeUSB, std::move(collection_info), |
| /*max_input_report_size=*/64, /*max_output_report_size=*/64, |
| /*max_feature_report_size=*/64); |
| mock_hid_service_->AddDevice(device_info); |
| return device_info; |
| } |
| |
| void UpdateTestDeviceWithNewTopLevelCollection() { |
| // Construct a device that will update the device added by |
| // AddTestDeviceWithTopLevelCollection when added to the HidService. |
| // The updated device info should have a new platform device ID and |
| // different collection info but the physical device ID and interface ID |
| // should be the same. |
| auto collection_info = mojom::HidCollectionInfo::New(); |
| collection_info->usage = mojom::HidUsageAndPage::New(1, 0xff00); |
| auto device_info = base::MakeRefCounted<HidDeviceInfo>( |
| kTestDeviceIds[3], "physical id 2", "interface id 2", /*vendor_id=*/0, |
| /*product_id=*/0, "Hid Service Unit Test", "HidDevice-2", |
| mojom::HidBusType::kHIDBusTypeUSB, std::move(collection_info), |
| /*max_input_report_size=*/128, /*max_output_report_size=*/128, |
| /*max_feature_report_size=*/0); |
| mock_hid_service_->AddDevice(device_info); |
| } |
| |
| mojo::Remote<mojom::HidManager> hid_manager_; |
| raw_ptr<MockHidService> mock_hid_service_; |
| }; |
| |
| // Test the GetDevices. |
| TEST_F(HidManagerTest, GetDevicesOnly) { |
| // Add two hid devices. |
| AddTestDevice0(); |
| AddTestDevice1(); |
| mock_hid_service_->FirstEnumerationComplete(); |
| |
| // Expect two devices will be received. |
| base::RunLoop run_loop; |
| hid_manager_->GetDevices(base::BindLambdaForTesting( |
| [&](std::vector<mojom::HidDeviceInfoPtr> devices) { |
| EXPECT_EQ(devices.size(), 2u); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| // Test the GetDevicesAndSetClient and the mojom::HidManagerClient |
| // interface. |
| TEST_F(HidManagerTest, GetDevicesAndSetClient) { |
| // Add one hid device. |
| auto device0 = AddTestDevice0(); |
| mock_hid_service_->FirstEnumerationComplete(); |
| |
| auto client = std::make_unique<MockHidManagerClient>(); |
| mojo::PendingAssociatedRemote<mojom::HidManagerClient> hid_manager_client; |
| client->Bind(hid_manager_client.InitWithNewEndpointAndPassReceiver()); |
| |
| // Call GetDevicesAndSetClient, expect 1 device will be received. |
| { |
| base::RunLoop run_loop; |
| hid_manager_->GetDevicesAndSetClient( |
| std::move(hid_manager_client), |
| base::BindLambdaForTesting( |
| [&](std::vector<mojom::HidDeviceInfoPtr> devices) { |
| EXPECT_EQ(devices.size(), 1u); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| // Add another hid device, expect MockHidManagerClient::DeviceAdded() will be |
| // called, and the guid should be same as expected. |
| { |
| scoped_refptr<HidDeviceInfo> device1; |
| base::RunLoop run_loop; |
| EXPECT_CALL(*client, DeviceAdded).WillOnce([&](auto d) { |
| ASSERT_TRUE(device1); |
| EXPECT_EQ(d->guid, device1->device_guid()); |
| run_loop.Quit(); |
| }); |
| device1 = AddTestDevice1(); |
| run_loop.Run(); |
| } |
| |
| // Remove one hid device, expect MockHidManagerClient::DeviceRemoved() will be |
| // called, and the guid should be same as expected. |
| { |
| base::RunLoop run_loop; |
| EXPECT_CALL(*client, DeviceRemoved).WillOnce([&](auto d) { |
| EXPECT_EQ(d->guid, device0->device_guid()); |
| run_loop.Quit(); |
| }); |
| mock_hid_service_->RemoveDevice(kTestDeviceIds[0]); |
| run_loop.Run(); |
| } |
| } |
| |
| TEST_F(HidManagerTest, DeviceChangedBeforeFirstEnumeration) { |
| // Add a hid device representing a single top-level collection, then update it |
| // with a second top-level collection before the first enumeration is |
| // complete. |
| auto device = AddTestDeviceWithTopLevelCollection(); |
| UpdateTestDeviceWithNewTopLevelCollection(); |
| mock_hid_service_->FirstEnumerationComplete(); |
| |
| auto client = std::make_unique<MockHidManagerClient>(); |
| mojo::PendingAssociatedRemote<mojom::HidManagerClient> hid_manager_client; |
| client->Bind(hid_manager_client.InitWithNewEndpointAndPassReceiver()); |
| |
| // Call GetDevicesAndSetClient, expect 1 device will be received. The device |
| // should include the updated device info. |
| base::RunLoop get_devices_loop; |
| hid_manager_->GetDevicesAndSetClient( |
| std::move(hid_manager_client), |
| base::BindLambdaForTesting( |
| [&](std::vector<mojom::HidDeviceInfoPtr> devices) { |
| EXPECT_EQ(devices.size(), 1u); |
| EXPECT_EQ(devices[0]->collections.size(), 2u); |
| EXPECT_EQ(devices[0]->max_input_report_size, 128u); |
| EXPECT_EQ(devices[0]->max_output_report_size, 128u); |
| EXPECT_EQ(devices[0]->max_feature_report_size, 64u); |
| get_devices_loop.Quit(); |
| })); |
| get_devices_loop.Run(); |
| |
| // Remove the hid device, expect MockHidManagerClient::DeviceRemoved() |
| // will be called, and the guid should be same as expected. |
| base::RunLoop device_removed_loop; |
| EXPECT_CALL(*client, DeviceRemoved).WillOnce([&](auto d) { |
| EXPECT_EQ(d->guid, device->device_guid()); |
| device_removed_loop.Quit(); |
| }); |
| mock_hid_service_->RemoveDevice(kTestDeviceIds[2]); |
| device_removed_loop.Run(); |
| |
| // Call GetDevices, expect no devices. |
| base::RunLoop get_devices_loop2; |
| hid_manager_->GetDevices(base::BindLambdaForTesting( |
| [&](std::vector<mojom::HidDeviceInfoPtr> devices) { |
| EXPECT_TRUE(devices.empty()); |
| get_devices_loop2.Quit(); |
| })); |
| get_devices_loop2.Run(); |
| } |
| |
| TEST_F(HidManagerTest, DeviceChangedAfterFirstEnumeration) { |
| // Add a hid device with a single top-level collection. |
| auto device = AddTestDeviceWithTopLevelCollection(); |
| mock_hid_service_->FirstEnumerationComplete(); |
| |
| auto client = std::make_unique<MockHidManagerClient>(); |
| mojo::PendingAssociatedRemote<mojom::HidManagerClient> hid_manager_client; |
| client->Bind(hid_manager_client.InitWithNewEndpointAndPassReceiver()); |
| |
| // Call GetDevicesAndSetClient, expect 1 device will be received. |
| base::RunLoop get_devices_loop; |
| hid_manager_->GetDevicesAndSetClient( |
| std::move(hid_manager_client), |
| base::BindLambdaForTesting( |
| [&](std::vector<mojom::HidDeviceInfoPtr> devices) { |
| EXPECT_EQ(devices.size(), 1u); |
| EXPECT_EQ(devices[0]->collections.size(), 1u); |
| EXPECT_EQ(devices[0]->max_input_report_size, 64u); |
| EXPECT_EQ(devices[0]->max_output_report_size, 64u); |
| EXPECT_EQ(devices[0]->max_feature_report_size, 64u); |
| get_devices_loop.Quit(); |
| })); |
| get_devices_loop.Run(); |
| |
| // Add a sibling hid device, expect MockHidManagerClient::DeviceChanged() will |
| // be called. Make sure the device info is updated. |
| base::RunLoop device_changed_loop; |
| EXPECT_CALL(*client, DeviceChanged).WillOnce([&](auto d) { |
| EXPECT_EQ(d->guid, device->device_guid()); |
| EXPECT_EQ(d->collections.size(), 2u); |
| EXPECT_EQ(d->max_input_report_size, 128u); |
| EXPECT_EQ(d->max_output_report_size, 128u); |
| EXPECT_EQ(d->max_feature_report_size, 64u); |
| device_changed_loop.Quit(); |
| }); |
| UpdateTestDeviceWithNewTopLevelCollection(); |
| device_changed_loop.Run(); |
| |
| // Remove the hid device, expect MockHidManagerClient::DeviceRemoved() |
| // will be called, and the guid should be same as expected. |
| base::RunLoop device_removed_loop; |
| EXPECT_CALL(*client, DeviceRemoved).WillOnce([&](auto d) { |
| EXPECT_EQ(d->guid, device->device_guid()); |
| device_removed_loop.Quit(); |
| }); |
| mock_hid_service_->RemoveDevice(kTestDeviceIds[2]); |
| device_removed_loop.Run(); |
| |
| // Call GetDevices, expect no devices. |
| base::RunLoop get_devices_loop2; |
| hid_manager_->GetDevices(base::BindLambdaForTesting( |
| [&](std::vector<mojom::HidDeviceInfoPtr> devices) { |
| EXPECT_TRUE(devices.empty()); |
| get_devices_loop2.Quit(); |
| })); |
| get_devices_loop2.Run(); |
| } |
| |
| TEST_F(HidManagerTest, DeviceAddedAndChangedAfterFirstEnumeration) { |
| mock_hid_service_->FirstEnumerationComplete(); |
| |
| auto client = std::make_unique<MockHidManagerClient>(); |
| mojo::PendingAssociatedRemote<mojom::HidManagerClient> hid_manager_client; |
| client->Bind(hid_manager_client.InitWithNewEndpointAndPassReceiver()); |
| |
| // Call GetDevicesAndSetClient, expect no devices. |
| base::RunLoop get_devices_loop; |
| hid_manager_->GetDevicesAndSetClient( |
| std::move(hid_manager_client), |
| base::BindLambdaForTesting( |
| [&](std::vector<mojom::HidDeviceInfoPtr> devices) { |
| EXPECT_TRUE(devices.empty()); |
| get_devices_loop.Quit(); |
| })); |
| get_devices_loop.Run(); |
| |
| // Add a hid device with a single top-level collection. |
| scoped_refptr<HidDeviceInfo> device; |
| base::RunLoop device_added_loop; |
| EXPECT_CALL(*client, DeviceAdded).WillOnce([&](auto d) { |
| EXPECT_EQ(d->guid, device->device_guid()); |
| EXPECT_EQ(d->collections.size(), 1u); |
| EXPECT_EQ(d->max_input_report_size, 64u); |
| EXPECT_EQ(d->max_output_report_size, 64u); |
| EXPECT_EQ(d->max_feature_report_size, 64u); |
| device_added_loop.Quit(); |
| }); |
| device = AddTestDeviceWithTopLevelCollection(); |
| device_added_loop.Run(); |
| |
| // Update the device, expect MockHidManagerClient::DeviceChanged() will be |
| // called. Make sure the collections and max report sizes are updated. |
| base::RunLoop device_changed_loop; |
| EXPECT_CALL(*client, DeviceChanged).WillOnce([&](auto d) { |
| EXPECT_EQ(d->guid, device->device_guid()); |
| EXPECT_EQ(d->collections.size(), 2u); |
| EXPECT_EQ(d->max_input_report_size, 128u); |
| EXPECT_EQ(d->max_output_report_size, 128u); |
| EXPECT_EQ(d->max_feature_report_size, 64u); |
| device_changed_loop.Quit(); |
| }); |
| UpdateTestDeviceWithNewTopLevelCollection(); |
| device_changed_loop.Run(); |
| |
| // Remove the device, expect MockHidManagerClient::DeviceRemoved() will be |
| // called and the guid should be same as expected. |
| base::RunLoop device_removed_loop; |
| EXPECT_CALL(*client, DeviceRemoved).WillOnce([&](auto d) { |
| EXPECT_EQ(d->guid, device->device_guid()); |
| device_removed_loop.Quit(); |
| }); |
| mock_hid_service_->RemoveDevice(kTestDeviceIds[2]); |
| device_removed_loop.Run(); |
| |
| // Call GetDevices, expect no devices. |
| base::RunLoop get_devices_loop2; |
| hid_manager_->GetDevices(base::BindLambdaForTesting( |
| [&](std::vector<mojom::HidDeviceInfoPtr> devices) { |
| EXPECT_TRUE(devices.empty()); |
| get_devices_loop2.Quit(); |
| })); |
| get_devices_loop2.Run(); |
| } |
| |
| // Test the Connect and the mojom::HidConnection interface. |
| TEST_F(HidManagerTest, TestHidConnectionInterface) { |
| // Add a hid device with a top-level collection. |
| auto device = AddTestDeviceWithTopLevelCollection(); |
| mock_hid_service_->FirstEnumerationComplete(); |
| |
| auto client = std::make_unique<MockHidManagerClient>(); |
| mojo::PendingAssociatedRemote<mojom::HidManagerClient> hid_manager_client; |
| client->Bind(hid_manager_client.InitWithNewEndpointAndPassReceiver()); |
| |
| // Call GetDevicesAndSetClient, expect 1 device will be received. |
| { |
| base::RunLoop run_loop; |
| hid_manager_->GetDevicesAndSetClient( |
| std::move(hid_manager_client), |
| base::BindLambdaForTesting( |
| [&](std::vector<mojom::HidDeviceInfoPtr> devices) { |
| EXPECT_EQ(devices.size(), 1u); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| // Connect and save the HidConnection InterfacePtr into MockHidManagerClient. |
| { |
| base::RunLoop run_loop; |
| hid_manager_->Connect( |
| device->device_guid(), |
| /*connection_client=*/mojo::NullRemote(), |
| /*watcher=*/mojo::NullRemote(), |
| /*allow_protected_reports=*/false, |
| /*allow_fido_reports=*/false, |
| base::BindLambdaForTesting( |
| [&](mojo::PendingRemote<mojom::HidConnection> connection) { |
| EXPECT_TRUE(connection); |
| client->SetConnection(std::move(connection)); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| // Test mojom::HidConnection::Read(). |
| { |
| base::RunLoop run_loop; |
| client->GetConnection()->Read(base::BindLambdaForTesting( |
| [&](bool success, uint8_t report_id, |
| const absl::optional<std::vector<uint8_t>>& buffer) { |
| constexpr base::StringPiece kExpected = "TestRead"; |
| EXPECT_TRUE(success); |
| EXPECT_EQ(report_id, 1u); |
| ASSERT_TRUE(buffer); |
| EXPECT_THAT(*buffer, ElementsAreArray(kExpected)); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| // Test mojom::HidConnection::Write(). |
| { |
| base::RunLoop run_loop; |
| client->GetConnection()->Write( |
| /*report_id=*/0, |
| /*buffer=*/{}, base::BindLambdaForTesting([&](bool success) { |
| EXPECT_TRUE(success); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| // Test mojom::HidConnection::GetFeatureReport(). |
| { |
| base::RunLoop run_loop; |
| client->GetConnection()->GetFeatureReport( |
| /*report_id=*/0, |
| base::BindLambdaForTesting( |
| [&](bool success, |
| const absl::optional<std::vector<uint8_t>>& buffer) { |
| constexpr base::StringPiece kExpected = "TestGetFeatureReport"; |
| EXPECT_TRUE(success); |
| ASSERT_TRUE(buffer); |
| EXPECT_THAT(*buffer, ElementsAreArray(kExpected)); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| // Test mojom::HidConnection::SendFeatureReport(). |
| { |
| base::RunLoop run_loop; |
| client->GetConnection()->SendFeatureReport( |
| /*report_id=*/0, |
| /*buffer=*/{}, base::BindLambdaForTesting([&](bool success) { |
| EXPECT_TRUE(success); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| } |
| |
| } // namespace |
| |
| } // namespace device |