| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <cstddef> |
| #include <memory> |
| #include <vector> |
| |
| #include "base/barrier_closure.h" |
| #include "base/functional/invoke.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/bind.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/mock_callback.h" |
| #include "base/test/test_future.h" |
| #include "content/browser/hid/hid_service.h" |
| #include "content/browser/hid/hid_test_utils.h" |
| #include "content/browser/service_worker/embedded_worker_test_helper.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/hid_delegate.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/test/back_forward_cache_util.h" |
| #include "content/public/test/navigation_simulator.h" |
| #include "content/public/test/test_browser_context.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/public/test/test_web_contents_factory.h" |
| #include "content/test/test_render_view_host.h" |
| #include "content/test/test_web_contents.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "mojo/public/cpp/test_support/fake_message_dispatch_context.h" |
| #include "mojo/public/cpp/test_support/test_utils.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 "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/mojom/hid/hid.mojom.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| using ::base::test::RunClosure; |
| using ::base::test::TestFuture; |
| using ::testing::_; |
| using ::testing::ByMove; |
| using ::testing::ElementsAre; |
| using ::testing::Expectation; |
| using ::testing::Invoke; |
| using ::testing::Return; |
| |
| enum HidServiceCreationType { |
| kCreateUsingRenderFrameHost, |
| kCreateUsingServiceWorkerContextCore, |
| }; |
| |
| const char kTestUrl[] = "https://www.google.com"; |
| const char kTestGuid[] = "test-guid"; |
| const char kCrossOriginTestUrl[] = "https://www.chromium.org"; |
| |
| std::string HidServiceCreationTypeToString(HidServiceCreationType type) { |
| switch (type) { |
| case kCreateUsingRenderFrameHost: |
| return "CreateUsingRenderFrameHost"; |
| case kCreateUsingServiceWorkerContextCore: |
| return "CreateUsingServiceWorkerContextCore"; |
| } |
| } |
| |
| 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 MockHidManagerClient : public device::mojom::HidManagerClient { |
| public: |
| MockHidManagerClient() = default; |
| MockHidManagerClient(MockHidManagerClient&) = delete; |
| MockHidManagerClient& operator=(MockHidManagerClient&) = delete; |
| ~MockHidManagerClient() override = default; |
| |
| void Bind(mojo::PendingAssociatedReceiver<device::mojom::HidManagerClient> |
| receiver) { |
| receiver_.Bind(std::move(receiver)); |
| } |
| |
| MOCK_METHOD1(DeviceAdded, void(device::mojom::HidDeviceInfoPtr device_info)); |
| MOCK_METHOD1(DeviceRemoved, |
| void(device::mojom::HidDeviceInfoPtr device_info)); |
| MOCK_METHOD1(DeviceChanged, |
| void(device::mojom::HidDeviceInfoPtr device_info)); |
| |
| private: |
| mojo::AssociatedReceiver<device::mojom::HidManagerClient> receiver_{this}; |
| }; |
| |
| class HidServiceTestHelper { |
| public: |
| HidServiceTestHelper() { |
| ON_CALL(hid_delegate(), GetHidManager).WillByDefault(Return(&hid_manager_)); |
| ON_CALL(hid_delegate(), IsFidoAllowedForOrigin) |
| .WillByDefault(Return(false)); |
| } |
| HidServiceTestHelper(HidServiceTestHelper&) = delete; |
| HidServiceTestHelper& operator=(HidServiceTestHelper&) = delete; |
| ~HidServiceTestHelper() = default; |
| |
| void ConnectDevice(const device::mojom::HidDeviceInfo& device) { |
| hid_manager_.AddDevice(device.Clone()); |
| hid_delegate().OnDeviceAdded(device); |
| } |
| |
| void DisconnectDevice(const device::mojom::HidDeviceInfo& device) { |
| hid_manager_.RemoveDevice(device.guid); |
| hid_delegate().OnDeviceRemoved(device); |
| } |
| |
| // Open a connection to |device|. |
| mojo::Remote<device::mojom::HidConnection> OpenDevice( |
| const mojo::Remote<blink::mojom::HidService>& hid_service, |
| device::mojom::HidDeviceInfoPtr& 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()); |
| EXPECT_TRUE(connection); |
| return connection; |
| } |
| |
| void UpdateDevice(const device::mojom::HidDeviceInfo& device) { |
| hid_manager_.ChangeDevice(device.Clone()); |
| hid_delegate().OnDeviceChanged(device); |
| } |
| |
| device::mojom::HidDeviceInfoPtr CreateDeviceWithNoReports() { |
| auto collection = device::mojom::HidCollectionInfo::New(); |
| collection->usage = device::mojom::HidUsageAndPage::New(1, 1); |
| auto device_info = device::mojom::HidDeviceInfo::New(); |
| device_info->guid = kTestGuid; |
| device_info->collections.push_back(std::move(collection)); |
| return device_info; |
| } |
| |
| device::mojom::HidDeviceInfoPtr CreateDeviceWithOneReport() { |
| auto device_info = CreateDeviceWithNoReports(); |
| auto collection = device::mojom::HidCollectionInfo::New(); |
| collection->usage = device::mojom::HidUsageAndPage::New(2, 2); |
| collection->input_reports.push_back( |
| device::mojom::HidReportDescription::New()); |
| device_info->collections.push_back(std::move(collection)); |
| return device_info; |
| } |
| |
| device::mojom::HidDeviceInfoPtr CreateDeviceWithTwoReports() { |
| auto device_info = CreateDeviceWithOneReport(); |
| auto collection = device::mojom::HidCollectionInfo::New(); |
| collection->usage = device::mojom::HidUsageAndPage::New(3, 3); |
| collection->output_reports.push_back( |
| device::mojom::HidReportDescription::New()); |
| device_info->collections.push_back(std::move(collection)); |
| return device_info; |
| } |
| |
| device::mojom::HidDeviceInfoPtr CreateFidoDevice() { |
| return device::CreateDeviceFromReportDescriptor( |
| /*vendor_id=*/0x1234, /*product_id=*/0xabcd, |
| device::TestReportDescriptors::FidoU2fHid()); |
| } |
| |
| MockHidDelegate& hid_delegate() { return test_client_.delegate(); } |
| FakeHidConnectionClient* connection_client() { return &connection_client_; } |
| device::FakeHidManager& hid_manager() { return hid_manager_; } |
| |
| private: |
| HidTestContentBrowserClient test_client_; |
| raw_ptr<ContentBrowserClient> original_client_ = nullptr; |
| device::FakeHidManager hid_manager_; |
| FakeHidConnectionClient connection_client_; |
| ScopedContentBrowserClientSetting setting{&test_client_}; |
| }; |
| |
| class HidServiceBaseTest : public testing::Test, public HidServiceTestHelper { |
| public: |
| HidServiceBaseTest() = default; |
| HidServiceBaseTest(HidServiceBaseTest&) = delete; |
| HidServiceBaseTest& operator=(HidServiceBaseTest&) = delete; |
| ~HidServiceBaseTest() override = default; |
| |
| mojo::Remote<blink::mojom::HidService>& GetService( |
| HidServiceCreationType type) { |
| switch (type) { |
| case kCreateUsingRenderFrameHost: |
| web_contents_ = |
| web_contents_factory_.CreateWebContents(&browser_context_); |
| static_cast<TestWebContents*>(web_contents_) |
| ->NavigateAndCommit(GURL(kTestUrl)); |
| static_cast<TestWebContents*>(web_contents_) |
| ->GetPrimaryMainFrame() |
| ->GetHidService(service_.BindNewPipeAndPassReceiver()); |
| break; |
| case kCreateUsingServiceWorkerContextCore: { |
| auto scope = GURL(kTestUrl); |
| auto origin = url::Origin::Create(scope); |
| auto worker_url = scope.Resolve("worker.js"); |
| embedded_worker_test_helper_ = |
| std::make_unique<EmbeddedWorkerTestHelper>(base::FilePath()); |
| EmbeddedWorkerTestHelper::RegistrationAndVersionPair pair = |
| embedded_worker_test_helper_->PrepareRegistrationAndVersion( |
| scope, worker_url); |
| worker_version_ = pair.second; |
| auto* embedded_worker = worker_version_->GetEmbeddedWorkerForTesting(); |
| embedded_worker_test_helper_->StartWorker( |
| embedded_worker, |
| embedded_worker_test_helper_->CreateStartParams(pair.second)); |
| EXPECT_CALL(hid_delegate(), IsServiceWorkerAllowedForOrigin(origin)) |
| .WillOnce(Return(true)); |
| embedded_worker->BindHidService(origin, |
| service_.BindNewPipeAndPassReceiver()); |
| break; |
| } |
| } |
| return service_; |
| } |
| |
| BrowserContext* GetBrowserContext(HidServiceCreationType type) { |
| switch (type) { |
| case kCreateUsingRenderFrameHost: |
| return &browser_context_; |
| case kCreateUsingServiceWorkerContextCore: |
| if (embedded_worker_test_helper_->context()) { |
| return embedded_worker_test_helper_->context() |
| ->wrapper() |
| ->browser_context(); |
| } |
| break; |
| } |
| NOTREACHED(); |
| return nullptr; |
| } |
| |
| void CheckHidServiceConnectedState(HidServiceCreationType type, |
| bool expected_state) { |
| if (type == kCreateUsingRenderFrameHost) { |
| ASSERT_EQ(web_contents_->IsConnectedToHidDevice(), expected_state); |
| } else if (type == kCreateUsingServiceWorkerContextCore) { |
| ASSERT_EQ(worker_version_->GetExternalRequestCountForTest(), |
| expected_state ? 1u : 0u); |
| } |
| } |
| |
| protected: |
| BrowserTaskEnvironment task_environment_; |
| mojo::Remote<blink::mojom::HidService> service_; |
| |
| // For create hid service using RenderFrameHost. |
| TestBrowserContext browser_context_; |
| TestWebContentsFactory web_contents_factory_; |
| raw_ptr<WebContents> web_contents_; // Owned by |web_contents_factory_|. |
| |
| // For create hid service using service worker. |
| std::unique_ptr<EmbeddedWorkerTestHelper> embedded_worker_test_helper_; |
| scoped_refptr<content::ServiceWorkerVersion> worker_version_; |
| }; |
| |
| class HidServiceRenderFrameHostTest : public RenderViewHostImplTestHarness, |
| public HidServiceTestHelper {}; |
| |
| class HidServiceTest |
| : public HidServiceBaseTest, |
| public testing::WithParamInterface<HidServiceCreationType> {}; |
| |
| // Test fixture parameterized for fido allowed or not. |
| class HidServiceFidoTest : public HidServiceBaseTest, |
| public testing::WithParamInterface< |
| std::tuple<HidServiceCreationType, bool>> {}; |
| |
| // Test fixture for service worker specific tests. |
| class HidServiceServiceWorkerBrowserContextDestroyedTest |
| : public HidServiceBaseTest { |
| public: |
| void DestroyBrowserContext() { |
| // Reset |embedded_worker_test_helper_| will subsequently destroy the |
| // BrowserContext associated with it. |
| embedded_worker_test_helper_.reset(); |
| } |
| |
| void SetUp() override { GetService(kCreateUsingServiceWorkerContextCore); } |
| }; |
| |
| } // namespace |
| |
| TEST_P(HidServiceTest, GetDevicesWithPermission) { |
| const auto& service = GetService(GetParam()); |
| |
| auto collection = device::mojom::HidCollectionInfo::New(); |
| collection->usage = device::mojom::HidUsageAndPage::New(0xff00, 0x0001); |
| collection->input_reports.push_back( |
| device::mojom::HidReportDescription::New()); |
| auto device_info = device::mojom::HidDeviceInfo::New(); |
| device_info->guid = kTestGuid; |
| device_info->collections.push_back(std::move(collection)); |
| ConnectDevice(*device_info); |
| |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| |
| base::RunLoop run_loop; |
| std::vector<device::mojom::HidDeviceInfoPtr> devices; |
| service->GetDevices(base::BindLambdaForTesting( |
| [&run_loop, &devices](std::vector<device::mojom::HidDeviceInfoPtr> d) { |
| devices = std::move(d); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| EXPECT_EQ(1u, devices.size()); |
| } |
| |
| TEST_P(HidServiceTest, GetDevicesWithoutPermission) { |
| const auto& service = GetService(GetParam()); |
| |
| auto device_info = CreateDeviceWithOneReport(); |
| ConnectDevice(*device_info); |
| |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(false)); |
| |
| base::RunLoop run_loop; |
| std::vector<device::mojom::HidDeviceInfoPtr> devices; |
| service->GetDevices(base::BindLambdaForTesting( |
| [&run_loop, &devices](std::vector<device::mojom::HidDeviceInfoPtr> d) { |
| devices = std::move(d); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| EXPECT_EQ(0u, devices.size()); |
| } |
| |
| TEST_P(HidServiceTest, RequestDevice) { |
| auto service_creation_type = GetParam(); |
| const auto& service = GetService(service_creation_type); |
| |
| auto device_info = CreateDeviceWithOneReport(); |
| std::vector<device::mojom::HidDeviceInfoPtr> device_infos; |
| device_infos.push_back(device_info.Clone()); |
| ConnectDevice(*device_info); |
| |
| if (service_creation_type == kCreateUsingRenderFrameHost) { |
| EXPECT_CALL(hid_delegate(), CanRequestDevicePermission) |
| .WillOnce(Return(true)); |
| EXPECT_CALL(hid_delegate(), RunChooserInternal) |
| .WillOnce(Return(ByMove(std::move(device_infos)))); |
| } |
| |
| base::RunLoop run_loop; |
| std::vector<device::mojom::HidDeviceInfoPtr> chosen_devices; |
| service->RequestDevice( |
| std::vector<blink::mojom::HidDeviceFilterPtr>(), |
| std::vector<blink::mojom::HidDeviceFilterPtr>(), |
| base::BindLambdaForTesting( |
| [&run_loop, |
| &chosen_devices](std::vector<device::mojom::HidDeviceInfoPtr> d) { |
| chosen_devices = std::move(d); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| if (service_creation_type == kCreateUsingRenderFrameHost) { |
| EXPECT_EQ(1u, chosen_devices.size()); |
| } else { |
| EXPECT_EQ(0u, chosen_devices.size()); |
| } |
| } |
| |
| TEST_P(HidServiceTest, OpenAndCloseHidConnection) { |
| auto service_creation_type = GetParam(); |
| const auto& service = GetService(service_creation_type); |
| |
| auto device_info = CreateDeviceWithOneReport(); |
| ConnectDevice(*device_info); |
| |
| mojo::PendingRemote<device::mojom::HidConnectionClient> hid_connection_client; |
| connection_client()->Bind( |
| hid_connection_client.InitWithNewPipeAndPassReceiver()); |
| |
| CheckHidServiceConnectedState(service_creation_type, false); |
| |
| base::RunLoop run_loop; |
| mojo::Remote<device::mojom::HidConnection> connection; |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillOnce(Return(device_info.get())); |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| EXPECT_CALL(hid_delegate(), |
| IncrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))); |
| service->Connect( |
| kTestGuid, std::move(hid_connection_client), |
| base::BindLambdaForTesting( |
| [&run_loop, |
| &connection](mojo::PendingRemote<device::mojom::HidConnection> c) { |
| connection.Bind(std::move(c)); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| EXPECT_TRUE(connection.is_connected()); |
| |
| CheckHidServiceConnectedState(service_creation_type, true); |
| |
| base::RunLoop disconnect_run_loop; |
| // Destroying |connection| will also disconnect the watcher. |
| EXPECT_CALL(hid_delegate(), |
| DecrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))) |
| .WillOnce(RunClosure(disconnect_run_loop.QuitClosure())); |
| connection.reset(); |
| |
| disconnect_run_loop.Run(); |
| CheckHidServiceConnectedState(service_creation_type, false); |
| } |
| |
| TEST_P(HidServiceTest, OpenHidConnectionFail) { |
| auto service_creation_type = GetParam(); |
| const auto& service = GetService(service_creation_type); |
| |
| // Note here no device is connected to the HID manager so opening connection |
| // will fail. |
| auto device_info = CreateDeviceWithOneReport(); |
| |
| mojo::PendingRemote<device::mojom::HidConnectionClient> hid_connection_client; |
| connection_client()->Bind( |
| hid_connection_client.InitWithNewPipeAndPassReceiver()); |
| |
| CheckHidServiceConnectedState(service_creation_type, false); |
| |
| base::RunLoop run_loop; |
| mojo::Remote<device::mojom::HidConnection> connection; |
| TestFuture<mojo::PendingRemote<device::mojom::HidConnection>> |
| pending_remote_future; |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillOnce(Return(device_info.get())); |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| EXPECT_CALL(hid_delegate(), |
| IncrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))); |
| EXPECT_CALL(hid_delegate(), |
| DecrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))) |
| .WillOnce(RunClosure(run_loop.QuitClosure())); |
| service->Connect(kTestGuid, std::move(hid_connection_client), |
| pending_remote_future.GetCallback()); |
| EXPECT_FALSE(pending_remote_future.Take()); |
| |
| run_loop.Run(); |
| CheckHidServiceConnectedState(service_creation_type, false); |
| } |
| |
| TEST_F(HidServiceRenderFrameHostTest, OpenAndNavigateCrossOrigin) { |
| // 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/1346021): Integrate WebHID with bfcache and remove this. |
| DisableBackForwardCacheForTesting(web_contents(), |
| BackForwardCache::TEST_REQUIRES_NO_CACHING); |
| |
| NavigateAndCommit(GURL(kTestUrl)); |
| |
| mojo::Remote<blink::mojom::HidService> service; |
| contents()->GetPrimaryMainFrame()->GetHidService( |
| service.BindNewPipeAndPassReceiver()); |
| |
| auto device_info = CreateDeviceWithOneReport(); |
| ConnectDevice(*device_info); |
| |
| mojo::PendingRemote<device::mojom::HidConnectionClient> hid_connection_client; |
| connection_client()->Bind( |
| hid_connection_client.InitWithNewPipeAndPassReceiver()); |
| |
| EXPECT_FALSE(contents()->IsConnectedToHidDevice()); |
| |
| base::RunLoop run_loop; |
| mojo::Remote<device::mojom::HidConnection> connection; |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillOnce(Return(device_info.get())); |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| EXPECT_CALL(hid_delegate(), |
| IncrementConnectionCount(browser_context(), |
| url::Origin::Create(GURL(kTestUrl)))); |
| service->Connect( |
| kTestGuid, std::move(hid_connection_client), |
| base::BindLambdaForTesting( |
| [&run_loop, |
| &connection](mojo::PendingRemote<device::mojom::HidConnection> c) { |
| connection.Bind(std::move(c)); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| EXPECT_TRUE(connection.is_connected()); |
| |
| EXPECT_TRUE(contents()->IsConnectedToHidDevice()); |
| |
| EXPECT_CALL(hid_delegate(), |
| DecrementConnectionCount(browser_context(), |
| url::Origin::Create(GURL(kTestUrl)))); |
| NavigateAndCommit(GURL(kCrossOriginTestUrl)); |
| |
| base::RunLoop disconnect_loop; |
| connection.set_disconnect_handler(disconnect_loop.QuitClosure()); |
| |
| disconnect_loop.Run(); |
| EXPECT_FALSE(contents()->IsConnectedToHidDevice()); |
| EXPECT_FALSE(connection.is_connected()); |
| } |
| |
| TEST_P(HidServiceTest, RegisterClient) { |
| MockHidManagerClient mock_hid_manager_client; |
| |
| base::RunLoop device_added_loop; |
| EXPECT_CALL(mock_hid_manager_client, DeviceAdded(_)) |
| .WillOnce(RunClosure(device_added_loop.QuitClosure())); |
| |
| base::RunLoop device_removed_loop; |
| EXPECT_CALL(mock_hid_manager_client, DeviceRemoved(_)) |
| .WillOnce(RunClosure(device_removed_loop.QuitClosure())); |
| |
| EXPECT_CALL(hid_delegate(), HasDevicePermission) |
| .WillOnce(Return(true)) |
| .WillOnce(Return(true)); |
| |
| const auto& service = GetService(GetParam()); |
| |
| mojo::PendingAssociatedRemote<device::mojom::HidManagerClient> |
| hid_manager_client; |
| mock_hid_manager_client.Bind( |
| hid_manager_client.InitWithNewEndpointAndPassReceiver()); |
| |
| // 1. Register the mock client with the service. Wait for GetDevices to |
| // return to ensure the client has been set. |
| service->RegisterClient(std::move(hid_manager_client)); |
| |
| base::RunLoop run_loop; |
| std::vector<device::mojom::HidDeviceInfoPtr> devices; |
| service->GetDevices(base::BindLambdaForTesting( |
| [&run_loop, &devices](std::vector<device::mojom::HidDeviceInfoPtr> d) { |
| devices = std::move(d); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| EXPECT_TRUE(devices.empty()); |
| |
| // 2. Connect a device and wait for DeviceAdded. |
| auto device_info = CreateDeviceWithOneReport(); |
| ConnectDevice(*device_info); |
| device_added_loop.Run(); |
| |
| // 3. Disconnect the device and wait for DeviceRemoved. |
| DisconnectDevice(*device_info); |
| device_removed_loop.Run(); |
| } |
| |
| TEST_P(HidServiceTest, RevokeDevicePermission) { |
| auto service_creation_type = GetParam(); |
| const auto& service = GetService(service_creation_type); |
| |
| // For now the device has permission. |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| |
| // Create a new device. |
| auto device_info = device::mojom::HidDeviceInfo::New(); |
| device_info->guid = kTestGuid; |
| ConnectDevice(*device_info); |
| |
| // Connect the device. |
| mojo::PendingRemote<device::mojom::HidConnectionClient> hid_connection_client; |
| connection_client()->Bind( |
| hid_connection_client.InitWithNewPipeAndPassReceiver()); |
| |
| CheckHidServiceConnectedState(service_creation_type, false); |
| |
| base::RunLoop run_loop; |
| mojo::Remote<device::mojom::HidConnection> connection; |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillOnce(Return(device_info.get())); |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| EXPECT_CALL(hid_delegate(), |
| IncrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))); |
| service->Connect( |
| kTestGuid, std::move(hid_connection_client), |
| base::BindLambdaForTesting( |
| [&run_loop, |
| &connection](mojo::PendingRemote<device::mojom::HidConnection> c) { |
| connection.Bind(std::move(c)); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| testing::Mock::VerifyAndClearExpectations(&hid_delegate()); |
| |
| CheckHidServiceConnectedState(service_creation_type, true); |
| EXPECT_TRUE(connection.is_connected()); |
| |
| base::RunLoop disconnect_loop; |
| connection.set_disconnect_handler(disconnect_loop.QuitClosure()); |
| |
| // Simulate user revoking permission. |
| url::Origin origin = url::Origin::Create(GURL(kTestUrl)); |
| EXPECT_CALL(hid_delegate(), |
| DecrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))); |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillOnce(Return(device_info.get())); |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(false)); |
| hid_delegate().OnPermissionRevoked(origin); |
| testing::Mock::VerifyAndClearExpectations(&hid_delegate()); |
| |
| disconnect_loop.Run(); |
| CheckHidServiceConnectedState(service_creation_type, false); |
| EXPECT_FALSE(connection.is_connected()); |
| } |
| |
| TEST_P(HidServiceTest, RevokeDevicePermissionWithoutConnection) { |
| auto service_creation_type = GetParam(); |
| GetService(service_creation_type); |
| |
| // Simulate user revoking permission. |
| url::Origin origin = url::Origin::Create(GURL(kTestUrl)); |
| hid_delegate().OnPermissionRevoked(origin); |
| |
| base::RunLoop().RunUntilIdle(); |
| CheckHidServiceConnectedState(service_creation_type, false); |
| } |
| |
| TEST_P(HidServiceTest, DeviceRemovedDisconnect) { |
| auto service_creation_type = GetParam(); |
| const auto& service = GetService(service_creation_type); |
| |
| // For now the device has permission. |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| |
| // Create a new device. |
| auto device_info = device::mojom::HidDeviceInfo::New(); |
| device_info->guid = kTestGuid; |
| ConnectDevice(*device_info); |
| |
| // Connect the device. |
| mojo::PendingRemote<device::mojom::HidConnectionClient> hid_connection_client; |
| connection_client()->Bind( |
| hid_connection_client.InitWithNewPipeAndPassReceiver()); |
| |
| CheckHidServiceConnectedState(service_creation_type, false); |
| |
| base::RunLoop run_loop; |
| mojo::Remote<device::mojom::HidConnection> connection; |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillOnce(Return(device_info.get())); |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| EXPECT_CALL(hid_delegate(), |
| IncrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))); |
| service->Connect( |
| kTestGuid, std::move(hid_connection_client), |
| base::BindLambdaForTesting( |
| [&run_loop, |
| &connection](mojo::PendingRemote<device::mojom::HidConnection> c) { |
| connection.Bind(std::move(c)); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| |
| CheckHidServiceConnectedState(service_creation_type, true); |
| EXPECT_TRUE(connection.is_connected()); |
| |
| base::RunLoop disconnect_loop; |
| connection.set_disconnect_handler(disconnect_loop.QuitClosure()); |
| |
| // Disconnect the device. |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| EXPECT_CALL(hid_delegate(), |
| DecrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))); |
| DisconnectDevice(*device_info); |
| |
| disconnect_loop.Run(); |
| CheckHidServiceConnectedState(service_creation_type, false); |
| EXPECT_FALSE(connection.is_connected()); |
| } |
| |
| TEST_P(HidServiceTest, DeviceChangedDoesNotDisconnect) { |
| auto service_creation_type = GetParam(); |
| const auto& service = GetService(service_creation_type); |
| |
| // Register the mock client with the service. Wait for GetDevices to return to |
| // ensure the client has been set. |
| MockHidManagerClient mock_hid_manager_client; |
| mojo::PendingAssociatedRemote<device::mojom::HidManagerClient> |
| hid_manager_client; |
| mock_hid_manager_client.Bind( |
| hid_manager_client.InitWithNewEndpointAndPassReceiver()); |
| service->RegisterClient(std::move(hid_manager_client)); |
| |
| base::RunLoop get_devices_loop; |
| service->GetDevices(base::BindLambdaForTesting( |
| [&](std::vector<device::mojom::HidDeviceInfoPtr> d) { |
| get_devices_loop.Quit(); |
| })); |
| get_devices_loop.Run(); |
| |
| // Create a new device. |
| base::RunLoop device_added_loop; |
| EXPECT_CALL(mock_hid_manager_client, DeviceAdded) |
| .WillOnce(RunClosure(device_added_loop.QuitClosure())); |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| auto device_info = CreateDeviceWithOneReport(); |
| ConnectDevice(*device_info); |
| device_added_loop.Run(); |
| |
| // Connect the device. |
| mojo::PendingRemote<device::mojom::HidConnectionClient> hid_connection_client; |
| connection_client()->Bind( |
| hid_connection_client.InitWithNewPipeAndPassReceiver()); |
| |
| CheckHidServiceConnectedState(service_creation_type, false); |
| |
| base::RunLoop run_loop; |
| mojo::Remote<device::mojom::HidConnection> connection; |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillOnce(Return(device_info.get())); |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| EXPECT_CALL(hid_delegate(), |
| IncrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))); |
| service->Connect( |
| kTestGuid, std::move(hid_connection_client), |
| base::BindLambdaForTesting( |
| [&](mojo::PendingRemote<device::mojom::HidConnection> c) { |
| connection.Bind(std::move(c)); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| |
| CheckHidServiceConnectedState(service_creation_type, true); |
| EXPECT_TRUE(connection.is_connected()); |
| |
| // Update the device info. Permissions are not affected. |
| auto updated_device_info = CreateDeviceWithTwoReports(); |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillOnce(Return(updated_device_info.get())); |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| UpdateDevice(*updated_device_info); |
| |
| // Make sure the device is still connected. |
| CheckHidServiceConnectedState(service_creation_type, true); |
| EXPECT_TRUE(connection.is_connected()); |
| |
| base::RunLoop disconnect_loop; |
| connection.set_disconnect_handler(disconnect_loop.QuitClosure()); |
| |
| // Simulate user revoking permission. |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(false)); |
| url::Origin origin = url::Origin::Create(GURL(kTestUrl)); |
| EXPECT_CALL(hid_delegate(), |
| DecrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))); |
| hid_delegate().OnPermissionRevoked(origin); |
| |
| disconnect_loop.Run(); |
| CheckHidServiceConnectedState(service_creation_type, false); |
| EXPECT_FALSE(connection.is_connected()); |
| } |
| |
| TEST_P(HidServiceTest, UnblockedDeviceChangedToBlockedDisconnects) { |
| auto service_creation_type = GetParam(); |
| const auto& service = GetService(service_creation_type); |
| |
| // Register the mock client with the service. Wait for GetDevices to return to |
| // ensure the client has been set. |
| MockHidManagerClient mock_hid_manager_client; |
| mojo::PendingAssociatedRemote<device::mojom::HidManagerClient> |
| hid_manager_client; |
| mock_hid_manager_client.Bind( |
| hid_manager_client.InitWithNewEndpointAndPassReceiver()); |
| service->RegisterClient(std::move(hid_manager_client)); |
| |
| base::RunLoop get_devices_loop; |
| service->GetDevices(base::BindLambdaForTesting( |
| [&](std::vector<device::mojom::HidDeviceInfoPtr> d) { |
| get_devices_loop.Quit(); |
| })); |
| get_devices_loop.Run(); |
| |
| // Create a new device. For now, the device has permission. |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| base::RunLoop device_added_loop; |
| EXPECT_CALL(mock_hid_manager_client, DeviceAdded) |
| .WillOnce(RunClosure(device_added_loop.QuitClosure())); |
| auto device_info = CreateDeviceWithOneReport(); |
| ConnectDevice(*device_info); |
| device_added_loop.Run(); |
| |
| // Connect the device. |
| mojo::PendingRemote<device::mojom::HidConnectionClient> hid_connection_client; |
| connection_client()->Bind( |
| hid_connection_client.InitWithNewPipeAndPassReceiver()); |
| |
| CheckHidServiceConnectedState(service_creation_type, false); |
| |
| base::RunLoop connect_loop; |
| mojo::Remote<device::mojom::HidConnection> connection; |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillOnce(Return(device_info.get())); |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| EXPECT_CALL(hid_delegate(), |
| IncrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))); |
| service->Connect( |
| kTestGuid, std::move(hid_connection_client), |
| base::BindLambdaForTesting( |
| [&](mojo::PendingRemote<device::mojom::HidConnection> c) { |
| connection.Bind(std::move(c)); |
| connect_loop.Quit(); |
| })); |
| connect_loop.Run(); |
| |
| CheckHidServiceConnectedState(service_creation_type, true); |
| EXPECT_TRUE(connection.is_connected()); |
| |
| // Update the device info. With the update, the device loses permission and |
| // the connection is closed. |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(false)); |
| EXPECT_CALL(mock_hid_manager_client, DeviceRemoved).Times(0); |
| EXPECT_CALL(mock_hid_manager_client, DeviceChanged).Times(0); |
| auto updated_device_info = device::mojom::HidDeviceInfo::New(); |
| updated_device_info->guid = kTestGuid; |
| base::RunLoop disconnect_loop; |
| connection.set_disconnect_handler(disconnect_loop.QuitClosure()); |
| EXPECT_CALL(hid_delegate(), |
| DecrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))); |
| UpdateDevice(*updated_device_info); |
| disconnect_loop.Run(); |
| |
| CheckHidServiceConnectedState(service_creation_type, false); |
| EXPECT_FALSE(connection.is_connected()); |
| } |
| |
| TEST_P(HidServiceTest, BlockedDeviceChangedToUnblockedDispatchesDeviceChanged) { |
| const auto& service = GetService(GetParam()); |
| |
| // Register the mock client with the service. Wait for GetDevices to return to |
| // ensure the client has been set. |
| MockHidManagerClient mock_hid_manager_client; |
| mojo::PendingAssociatedRemote<device::mojom::HidManagerClient> |
| hid_manager_client; |
| mock_hid_manager_client.Bind( |
| hid_manager_client.InitWithNewEndpointAndPassReceiver()); |
| service->RegisterClient(std::move(hid_manager_client)); |
| |
| base::RunLoop get_devices_loop; |
| service->GetDevices(base::BindLambdaForTesting( |
| [&](std::vector<device::mojom::HidDeviceInfoPtr> d) { |
| get_devices_loop.Quit(); |
| })); |
| get_devices_loop.Run(); |
| |
| // Create a new device. The device is blocked because it has no reports. |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| EXPECT_CALL(mock_hid_manager_client, DeviceAdded).Times(0); |
| auto device_info = CreateDeviceWithNoReports(); |
| ConnectDevice(*device_info); |
| |
| // Update the device. After the update, the device has an input report and is |
| // no longer blocked. The DeviceChanged event should be dispatched to the |
| // client. |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| base::RunLoop device_changed_loop; |
| EXPECT_CALL(mock_hid_manager_client, DeviceChanged) |
| .WillOnce(RunClosure(device_changed_loop.QuitClosure())); |
| auto updated_device_info = CreateDeviceWithOneReport(); |
| UpdateDevice(*updated_device_info); |
| device_changed_loop.Run(); |
| |
| // Disconnect the device. DeviceRemoved should be dispatched to the client. |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| base::RunLoop device_removed_loop; |
| EXPECT_CALL(mock_hid_manager_client, DeviceRemoved) |
| .WillOnce(RunClosure(device_removed_loop.QuitClosure())); |
| DisconnectDevice(*updated_device_info); |
| device_removed_loop.Run(); |
| } |
| |
| TEST_P(HidServiceTest, Forget) { |
| auto service_creation_type = GetParam(); |
| const auto& service = GetService(service_creation_type); |
| |
| // For now the device has permission. |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillRepeatedly(Return(true)); |
| |
| // Create a new device. |
| auto device_info = device::mojom::HidDeviceInfo::New(); |
| device_info->guid = kTestGuid; |
| ConnectDevice(*device_info); |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillRepeatedly(Return(device_info.get())); |
| |
| // Connect the device. |
| mojo::PendingRemote<device::mojom::HidConnectionClient> hid_connection_client; |
| connection_client()->Bind( |
| hid_connection_client.InitWithNewPipeAndPassReceiver()); |
| |
| CheckHidServiceConnectedState(service_creation_type, false); |
| |
| TestFuture<mojo::PendingRemote<device::mojom::HidConnection>> |
| future_connection; |
| EXPECT_CALL(hid_delegate(), |
| IncrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))); |
| service->Connect(kTestGuid, std::move(hid_connection_client), |
| future_connection.GetCallback()); |
| mojo::Remote<device::mojom::HidConnection> connection( |
| future_connection.Take()); |
| |
| CheckHidServiceConnectedState(service_creation_type, true); |
| EXPECT_TRUE(connection.is_connected()); |
| |
| base::RunLoop disconnect_loop; |
| connection.set_disconnect_handler(disconnect_loop.QuitClosure()); |
| |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(false)); |
| EXPECT_CALL(hid_delegate(), RevokeDevicePermission) |
| .WillOnce([this](content::BrowserContext* browser_context, |
| const url::Origin& origin, |
| const device::mojom::HidDeviceInfo& device) { |
| hid_delegate().OnPermissionRevoked(origin); |
| }); |
| base::MockCallback<blink::mojom::HidService::ForgetCallback> forget_callback; |
| EXPECT_CALL(forget_callback, Run); |
| EXPECT_CALL(hid_delegate(), |
| DecrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))); |
| service->Forget(std::move(device_info), forget_callback.Get()); |
| |
| disconnect_loop.Run(); |
| CheckHidServiceConnectedState(service_creation_type, false); |
| EXPECT_FALSE(connection.is_connected()); |
| } |
| |
| TEST_P(HidServiceTest, OpenDevicesThenRemoveDevices) { |
| auto service_creation_type = GetParam(); |
| const auto& service = GetService(service_creation_type); |
| |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillRepeatedly(Return(true)); |
| |
| size_t num_devices = 5; |
| std::vector<device::mojom::HidDeviceInfoPtr> devices; |
| for (size_t device_idx = 0; device_idx < num_devices; device_idx++) { |
| auto device_info = device::mojom::HidDeviceInfo::New(); |
| device_info->guid = base::NumberToString(device_idx); |
| ConnectDevice(*device_info); |
| devices.push_back(std::move(device_info)); |
| } |
| CheckHidServiceConnectedState(service_creation_type, false); |
| |
| std::vector<mojo::Remote<device::mojom::HidConnection>> connections; |
| std::vector<FakeHidConnectionClient> connection_clients(num_devices); |
| EXPECT_CALL(hid_delegate(), IncrementConnectionCount).Times(num_devices); |
| for (size_t device_idx = 0; device_idx < num_devices; device_idx++) { |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillOnce(Return(devices[device_idx].get())); |
| connections.push_back(OpenDevice(service, devices[device_idx], |
| connection_clients[device_idx])); |
| } |
| CheckHidServiceConnectedState(service_creation_type, true); |
| |
| base::RunLoop run_loop; |
| auto barrier = base::BarrierClosure(num_devices, run_loop.QuitClosure()); |
| EXPECT_CALL(hid_delegate(), DecrementConnectionCount) |
| .Times(num_devices) |
| .WillRepeatedly(RunClosure(barrier)); |
| for (const auto& device : devices) { |
| DisconnectDevice(*device); |
| } |
| run_loop.Run(); |
| CheckHidServiceConnectedState(service_creation_type, false); |
| } |
| |
| TEST_P(HidServiceTest, OpenDevicesThenRevokePermission) { |
| auto service_creation_type = GetParam(); |
| const auto& service = GetService(service_creation_type); |
| |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillRepeatedly(Return(true)); |
| |
| size_t num_devices = 5; |
| std::vector<device::mojom::HidDeviceInfoPtr> devices; |
| for (size_t device_idx = 0; device_idx < num_devices; device_idx++) { |
| auto device_info = device::mojom::HidDeviceInfo::New(); |
| device_info->guid = base::NumberToString(device_idx); |
| ConnectDevice(*device_info); |
| devices.push_back(std::move(device_info)); |
| } |
| CheckHidServiceConnectedState(service_creation_type, false); |
| |
| std::vector<mojo::Remote<device::mojom::HidConnection>> connections; |
| std::vector<FakeHidConnectionClient> connection_clients(num_devices); |
| EXPECT_CALL(hid_delegate(), IncrementConnectionCount).Times(num_devices); |
| for (size_t device_idx = 0; device_idx < num_devices; device_idx++) { |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillOnce(Return(devices[device_idx].get())); |
| connections.push_back(OpenDevice(service, devices[device_idx], |
| connection_clients[device_idx])); |
| } |
| CheckHidServiceConnectedState(service_creation_type, true); |
| |
| // Simulate user revoking permission. |
| EXPECT_CALL(hid_delegate(), HasDevicePermission) |
| .WillRepeatedly(Return(false)); |
| for (const auto& device : devices) { |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo(_, device->guid)) |
| .WillOnce(Return(device.get())); |
| } |
| |
| base::RunLoop run_loop; |
| auto barrier = base::BarrierClosure(num_devices, run_loop.QuitClosure()); |
| url::Origin origin = url::Origin::Create(GURL(kTestUrl)); |
| EXPECT_CALL(hid_delegate(), |
| DecrementConnectionCount(GetBrowserContext(service_creation_type), |
| origin)) |
| .Times(num_devices) |
| .WillRepeatedly(RunClosure(barrier)); |
| hid_delegate().OnPermissionRevoked(origin); |
| |
| CheckHidServiceConnectedState(service_creation_type, false); |
| } |
| |
| TEST_P(HidServiceTest, OpenDevicesThenHidServiceReset) { |
| auto service_creation_type = GetParam(); |
| auto& service = GetService(service_creation_type); |
| |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillRepeatedly(Return(true)); |
| |
| size_t num_devices = 5; |
| std::vector<device::mojom::HidDeviceInfoPtr> devices; |
| for (size_t device_idx = 0; device_idx < num_devices; device_idx++) { |
| auto device_info = device::mojom::HidDeviceInfo::New(); |
| device_info->guid = base::NumberToString(device_idx); |
| ConnectDevice(*device_info); |
| devices.push_back(std::move(device_info)); |
| } |
| CheckHidServiceConnectedState(service_creation_type, false); |
| |
| std::vector<mojo::Remote<device::mojom::HidConnection>> connections; |
| std::vector<FakeHidConnectionClient> connection_clients(num_devices); |
| EXPECT_CALL(hid_delegate(), IncrementConnectionCount).Times(num_devices); |
| for (size_t device_idx = 0; device_idx < num_devices; device_idx++) { |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillOnce(Return(devices[device_idx].get())); |
| connections.push_back(OpenDevice(service, devices[device_idx], |
| connection_clients[device_idx])); |
| } |
| CheckHidServiceConnectedState(service_creation_type, true); |
| |
| base::RunLoop run_loop; |
| auto barrier = base::BarrierClosure(num_devices, run_loop.QuitClosure()); |
| EXPECT_CALL(hid_delegate(), DecrementConnectionCount) |
| .Times(num_devices) |
| .WillRepeatedly(RunClosure(barrier)); |
| service.reset(); |
| run_loop.Run(); |
| CheckHidServiceConnectedState(service_creation_type, false); |
| } |
| |
| TEST_P(HidServiceFidoTest, FidoDeviceAllowedWithPrivilegedOrigin) { |
| auto service_creation_type = std::get<0>(GetParam()); |
| const auto& service = GetService(service_creation_type); |
| const bool is_fido_allowed = std::get<1>(GetParam()); |
| |
| // Register the mock client with the service. |
| MockHidManagerClient mock_hid_manager_client; |
| mojo::PendingAssociatedRemote<device::mojom::HidManagerClient> |
| hid_manager_client; |
| mock_hid_manager_client.Bind( |
| hid_manager_client.InitWithNewEndpointAndPassReceiver()); |
| service->RegisterClient(std::move(hid_manager_client)); |
| |
| // Wait for GetDevices to return to ensure the client has been set. HidService |
| // checks if the origin is allowed to access FIDO reports before returning the |
| // device information to the client. |
| url::Origin origin = url::Origin::Create(GURL(kTestUrl)); |
| EXPECT_CALL(hid_delegate(), IsFidoAllowedForOrigin(_, origin)) |
| .WillOnce(Return(is_fido_allowed)); |
| base::RunLoop get_devices_loop; |
| service->GetDevices(base::BindLambdaForTesting( |
| [&](std::vector<device::mojom::HidDeviceInfoPtr> d) { |
| EXPECT_TRUE(d.empty()); |
| get_devices_loop.Quit(); |
| })); |
| get_devices_loop.Run(); |
| |
| // Create a FIDO device with two reports. Both reports are protected, which |
| // would normally cause the device to be blocked. |
| auto device_info = CreateFidoDevice(); |
| ASSERT_EQ(device_info->collections.size(), 1u); |
| ASSERT_EQ(device_info->collections[0]->input_reports.size(), 1u); |
| EXPECT_EQ(device_info->collections[0]->input_reports[0]->report_id, 0u); |
| ASSERT_EQ(device_info->collections[0]->output_reports.size(), 1u); |
| EXPECT_EQ(device_info->collections[0]->output_reports[0]->report_id, 0u); |
| EXPECT_TRUE(device_info->collections[0]->feature_reports.empty()); |
| ASSERT_TRUE(device_info->protected_input_report_ids); |
| EXPECT_THAT(*device_info->protected_input_report_ids, ElementsAre(0)); |
| ASSERT_TRUE(device_info->protected_output_report_ids); |
| EXPECT_THAT(*device_info->protected_output_report_ids, ElementsAre(0)); |
| ASSERT_TRUE(device_info->protected_output_report_ids); |
| EXPECT_TRUE(device_info->protected_feature_report_ids->empty()); |
| |
| // Add the device to the HidManager. HidService checks if the origin is |
| // allowed to access FIDO reports before dispatching DeviceAdded to its |
| // clients. If the origin is allowed to access FIDO reports, the |
| // information about those reports should be included. If the origin is not |
| // allowed to access FIDO reports, the device is blocked and DeviceAdded is |
| // not called. |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| EXPECT_CALL(hid_delegate(), IsFidoAllowedForOrigin(_, origin)) |
| .WillOnce(Return(is_fido_allowed)); |
| base::RunLoop device_added_loop; |
| if (is_fido_allowed) { |
| EXPECT_CALL(mock_hid_manager_client, DeviceAdded).WillOnce([&](auto d) { |
| EXPECT_EQ(d->collections.size(), 1u); |
| if (!d->collections.empty()) { |
| EXPECT_EQ(d->collections[0]->input_reports.size(), 1u); |
| EXPECT_EQ(d->collections[0]->output_reports.size(), 1u); |
| EXPECT_EQ(d->collections[0]->feature_reports.size(), 0u); |
| } |
| device_added_loop.Quit(); |
| }); |
| } |
| ConnectDevice(*device_info); |
| if (is_fido_allowed) |
| device_added_loop.Run(); |
| |
| // Update the device. HidService checks if the origin is allowed to access |
| // FIDO reports before dispatching DeviceChanged to its clients. |
| // |
| // The updated device includes a second top-level collection containing a |
| // feature report. The second top-level collection does not have a protected |
| // usage and should be included whether or not the origin is allowed to access |
| // FIDO reports. |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| EXPECT_CALL(hid_delegate(), IsFidoAllowedForOrigin(_, origin)) |
| .WillOnce(Return(is_fido_allowed)); |
| base::RunLoop device_changed_loop; |
| EXPECT_CALL(mock_hid_manager_client, DeviceChanged).WillOnce([&](auto d) { |
| if (is_fido_allowed) { |
| EXPECT_EQ(d->collections.size(), 2u); |
| if (d->collections.size() >= 2) { |
| EXPECT_EQ(d->collections[0]->input_reports.size(), 1u); |
| EXPECT_EQ(d->collections[0]->output_reports.size(), 1u); |
| EXPECT_EQ(d->collections[0]->feature_reports.size(), 0u); |
| EXPECT_EQ(d->collections[1]->input_reports.size(), 0u); |
| EXPECT_EQ(d->collections[1]->output_reports.size(), 0u); |
| EXPECT_EQ(d->collections[1]->feature_reports.size(), 1u); |
| } |
| } else { |
| EXPECT_EQ(d->collections.size(), 1u); |
| if (d->collections.size() >= 1) { |
| EXPECT_EQ(d->collections[0]->input_reports.size(), 0u); |
| EXPECT_EQ(d->collections[0]->output_reports.size(), 0u); |
| EXPECT_EQ(d->collections[0]->feature_reports.size(), 1u); |
| } |
| } |
| device_changed_loop.Quit(); |
| }); |
| auto collection = device::mojom::HidCollectionInfo::New(); |
| collection->usage = device::mojom::HidUsageAndPage::New( |
| device::mojom::kGenericDesktopJoystick, |
| device::mojom::kPageGenericDesktop); |
| collection->collection_type = device::mojom::kHIDCollectionTypeApplication; |
| collection->feature_reports.push_back( |
| device::mojom::HidReportDescription::New()); |
| auto updated_device_info = device_info.Clone(); |
| updated_device_info->collections.push_back(std::move(collection)); |
| UpdateDevice(*updated_device_info); |
| device_changed_loop.Run(); |
| |
| // Open a connection. HidService checks if the origin is allowed to access |
| // FIDO reports before creating a HidConnection. |
| EXPECT_CALL(hid_delegate(), IsFidoAllowedForOrigin(_, origin)) |
| .WillOnce(Return(is_fido_allowed)); |
| mojo::PendingRemote<device::mojom::HidConnectionClient> hid_connection_client; |
| connection_client()->Bind( |
| hid_connection_client.InitWithNewPipeAndPassReceiver()); |
| base::RunLoop connect_loop; |
| mojo::Remote<device::mojom::HidConnection> connection; |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillOnce(Return(device_info.get())); |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| EXPECT_CALL(hid_delegate(), |
| IncrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))); |
| service->Connect( |
| device_info->guid, std::move(hid_connection_client), |
| base::BindLambdaForTesting( |
| [&](mojo::PendingRemote<device::mojom::HidConnection> c) { |
| connection.Bind(std::move(c)); |
| connect_loop.Quit(); |
| })); |
| connect_loop.Run(); |
| EXPECT_TRUE(connection.is_connected()); |
| |
| // Try reading from the connection. The read should succeed if the connection |
| // is allowed to receive FIDO reports. |
| base::RunLoop read_loop; |
| connection->Read(base::BindLambdaForTesting( |
| [&](bool success, uint8_t report_id, |
| const absl::optional<std::vector<uint8_t>>& buffer) { |
| EXPECT_EQ(success, is_fido_allowed); |
| read_loop.Quit(); |
| })); |
| read_loop.Run(); |
| |
| // Try writing to the connection. The write should succeed if the connection |
| // is allowed to send FIDO reports. |
| // |
| // Writing to FakeHidConnection will only succeed if the report data is |
| // exactly "o-report". |
| base::RunLoop write_loop; |
| std::vector<uint8_t> buffer = {'o', '-', 'r', 'e', 'p', 'o', 'r', 't'}; |
| connection->Write(/*report_id=*/0, buffer, |
| base::BindLambdaForTesting([&](bool success) { |
| EXPECT_EQ(success, is_fido_allowed); |
| write_loop.Quit(); |
| })); |
| write_loop.Run(); |
| |
| // Disconnect the device. HidService checks if the origin is allowed to access |
| // FIDO reports before dispatching DeviceRemoved to its clients. The |
| // information about FIDO reports should only be included if the origin is |
| // allowed to access FIDO reports. |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(true)); |
| EXPECT_CALL(hid_delegate(), IsFidoAllowedForOrigin(_, origin)) |
| .WillOnce(Return(is_fido_allowed)); |
| base::RunLoop device_removed_loop; |
| EXPECT_CALL(mock_hid_manager_client, DeviceRemoved).WillOnce([&](auto d) { |
| if (is_fido_allowed) { |
| EXPECT_EQ(d->collections.size(), 2u); |
| if (d->collections.size() >= 2) { |
| EXPECT_EQ(d->collections[0]->input_reports.size(), 1u); |
| EXPECT_EQ(d->collections[0]->output_reports.size(), 1u); |
| EXPECT_EQ(d->collections[0]->feature_reports.size(), 0u); |
| EXPECT_EQ(d->collections[1]->input_reports.size(), 0u); |
| EXPECT_EQ(d->collections[1]->output_reports.size(), 0u); |
| EXPECT_EQ(d->collections[1]->feature_reports.size(), 1u); |
| } |
| } else { |
| EXPECT_EQ(d->collections.size(), 1u); |
| if (d->collections.size() >= 1) { |
| EXPECT_EQ(d->collections[0]->input_reports.size(), 0u); |
| EXPECT_EQ(d->collections[0]->output_reports.size(), 0u); |
| EXPECT_EQ(d->collections[0]->feature_reports.size(), 1u); |
| } |
| } |
| device_removed_loop.Quit(); |
| }); |
| EXPECT_CALL(hid_delegate(), |
| DecrementConnectionCount(GetBrowserContext(service_creation_type), |
| url::Origin::Create(GURL(kTestUrl)))); |
| DisconnectDevice(*updated_device_info); |
| device_removed_loop.Run(); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| HidServiceTests, |
| HidServiceTest, |
| testing::Values(kCreateUsingRenderFrameHost, |
| kCreateUsingServiceWorkerContextCore), |
| [](const ::testing::TestParamInfo<HidServiceCreationType>& info) { |
| return HidServiceCreationTypeToString(info.param); |
| }); |
| |
| const bool kIsFidoAllowed[]{true, false}; |
| INSTANTIATE_TEST_SUITE_P( |
| HidServiceFidoTests, |
| HidServiceFidoTest, |
| testing::Combine(testing::Values(kCreateUsingRenderFrameHost, |
| kCreateUsingServiceWorkerContextCore), |
| testing::ValuesIn(kIsFidoAllowed)), |
| [](const ::testing::TestParamInfo<std::tuple<HidServiceCreationType, bool>>& |
| info) { |
| return base::StringPrintf( |
| "%s_%s", |
| HidServiceCreationTypeToString(std::get<0>(info.param)).c_str(), |
| std::get<1>(info.param) ? "FidoAllowed" : "FidoNotAllowed"); |
| }); |
| |
| TEST_F(HidServiceServiceWorkerBrowserContextDestroyedTest, GetDevices) { |
| auto device_info = CreateDeviceWithOneReport(); |
| ConnectDevice(*device_info); |
| DestroyBrowserContext(); |
| |
| TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> devices_future; |
| service_->GetDevices(devices_future.GetCallback()); |
| EXPECT_EQ(0u, devices_future.Get().size()); |
| } |
| |
| TEST_F(HidServiceServiceWorkerBrowserContextDestroyedTest, Connect) { |
| auto device_info = CreateDeviceWithOneReport(); |
| ConnectDevice(*device_info); |
| mojo::PendingRemote<device::mojom::HidConnectionClient> hid_connection_client; |
| connection_client()->Bind( |
| hid_connection_client.InitWithNewPipeAndPassReceiver()); |
| DestroyBrowserContext(); |
| |
| TestFuture<mojo::PendingRemote<device::mojom::HidConnection>> |
| connection_future; |
| service_->Connect(kTestGuid, std::move(hid_connection_client), |
| connection_future.GetCallback()); |
| EXPECT_FALSE(connection_future.Get().is_valid()); |
| } |
| |
| TEST_F(HidServiceServiceWorkerBrowserContextDestroyedTest, Forget) { |
| auto device_info = CreateDeviceWithOneReport(); |
| ConnectDevice(*device_info); |
| |
| DestroyBrowserContext(); |
| EXPECT_CALL(hid_delegate(), RevokeDevicePermission).Times(0); |
| base::RunLoop run_loop; |
| service_->Forget(std::move(device_info), run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| TEST_F(HidServiceServiceWorkerBrowserContextDestroyedTest, RejectOpaqueOrigin) { |
| // Create a fake dispatch context to trigger a bad message in. |
| mojo::FakeMessageDispatchContext fake_dispatch_context; |
| mojo::test::BadMessageObserver bad_message_observer; |
| |
| auto response_headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>(std::string()); |
| response_headers->SetHeader("Content-Security-Policy", |
| "sandbox allow-scripts"); |
| auto* web_contents = static_cast<TestWebContents*>( |
| web_contents_factory_.CreateWebContents(&browser_context_)); |
| auto navigation_simulator = NavigationSimulator::CreateRendererInitiated( |
| GURL("http://whatever.com"), web_contents->GetPrimaryMainFrame()); |
| navigation_simulator->SetResponseHeaders(response_headers); |
| navigation_simulator->Start(); |
| navigation_simulator->Commit(); |
| |
| mojo::Remote<blink::mojom::HidService> service; |
| web_contents->GetPrimaryMainFrame()->GetHidService( |
| service.BindNewPipeAndPassReceiver()); |
| |
| EXPECT_EQ(bad_message_observer.WaitForBadMessage(), |
| "WebHID is not allowed from an opaque origin."); |
| } |
| |
| TEST_P(HidServiceTest, ConnectionFailedWithoutPermission) { |
| auto service_creation_type = GetParam(); |
| const auto& service = GetService(service_creation_type); |
| |
| // Create a new device. |
| auto device_info = device::mojom::HidDeviceInfo::New(); |
| device_info->guid = kTestGuid; |
| ConnectDevice(*device_info); |
| |
| // Connect the device. |
| mojo::PendingRemote<device::mojom::HidConnectionClient> hid_connection_client; |
| connection_client()->Bind( |
| hid_connection_client.InitWithNewPipeAndPassReceiver()); |
| |
| CheckHidServiceConnectedState(service_creation_type, false); |
| |
| base::RunLoop run_loop; |
| mojo::Remote<device::mojom::HidConnection> connection; |
| TestFuture<mojo::PendingRemote<device::mojom::HidConnection>> |
| pending_remote_future; |
| EXPECT_CALL(hid_delegate(), GetDeviceInfo) |
| .WillOnce(Return(device_info.get())); |
| EXPECT_CALL(hid_delegate(), HasDevicePermission).WillOnce(Return(false)); |
| service->Connect(kTestGuid, std::move(hid_connection_client), |
| pending_remote_future.GetCallback()); |
| EXPECT_FALSE(pending_remote_future.Take()); |
| run_loop.RunUntilIdle(); |
| CheckHidServiceConnectedState(service_creation_type, false); |
| } |
| |
| } // namespace content |