// 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 #include #include "base/command_line.h" #include "base/memory/raw_ptr.h" #include "base/strings/stringprintf.h" #include "content/browser/hid/hid_test_utils.h" #include "content/public/browser/content_browser_client.h" #include "content/public/browser/hid_chooser.h" #include "content/public/browser/hid_delegate.h" #include "content/public/common/content_client.h" #include "content/public/common/content_switches.h" #include "content/public/test/browser_test.h" #include "content/public/test/browser_test_utils.h" #include "content/public/test/content_browser_test.h" #include "content/public/test/content_browser_test_utils.h" #include "content/public/test/fenced_frame_test_util.h" #include "content/shell/browser/shell.h" #include "services/device/public/cpp/test/fake_hid_manager.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" using testing::ByMove; using testing::Exactly; using testing::Return; namespace content { namespace { // Create a device with a single collection containing an input report and an // output report. Both reports have report ID 0. device::mojom::HidDeviceInfoPtr CreateTestDeviceWithInputAndOutputReports() { auto collection = device::mojom::HidCollectionInfo::New(); collection->usage = device::mojom::HidUsageAndPage::New(0x0001, 0xff00); collection->input_reports.push_back( device::mojom::HidReportDescription::New()); collection->output_reports.push_back( device::mojom::HidReportDescription::New()); auto device = device::mojom::HidDeviceInfo::New(); device->guid = "test-guid"; device->collections.push_back(std::move(collection)); return device; } class HidTest : public ContentBrowserTest { public: HidTest() { ON_CALL(delegate(), GetHidManager).WillByDefault(Return(&hid_manager_)); } ~HidTest() override = default; void SetUpCommandLine(base::CommandLine* command_line) override { ContentBrowserTest::SetUpCommandLine(command_line); command_line->AppendSwitch( switches::kEnableExperimentalWebPlatformFeatures); } void SetUpOnMainThread() override { original_client_ = SetBrowserClientForTesting(&test_client_); } void TearDownOnMainThread() override { if (original_client_) SetBrowserClientForTesting(original_client_); } MockHidDelegate& delegate() { return test_client_.delegate(); } device::FakeHidManager* hid_manager() { return &hid_manager_; } private: HidTestContentBrowserClient test_client_; raw_ptr original_client_ = nullptr; device::FakeHidManager hid_manager_; }; } // namespace IN_PROC_BROWSER_TEST_F(HidTest, GetDevices) { EXPECT_TRUE(NavigateToURL(shell(), GetTestUrl(nullptr, "simple_page.html"))); // Three devices are added but only two will have permission granted. for (int i = 0; i < 3; i++) { auto device = CreateTestDeviceWithInputAndOutputReports(); device->guid = base::StringPrintf("test-guid-%02d", i); hid_manager()->AddDevice(std::move(device)); } EXPECT_CALL(delegate(), HasDevicePermission) .WillOnce(Return(true)) .WillOnce(Return(false)) .WillOnce(Return(true)); EXPECT_EQ( 2, EvalJs(shell(), R"(navigator.hid.getDevices().then(devices => devices.length))")); } IN_PROC_BROWSER_TEST_F(HidTest, RequestDevice) { EXPECT_TRUE(NavigateToURL(shell(), GetTestUrl(nullptr, "simple_page.html"))); EXPECT_CALL(delegate(), CanRequestDevicePermission).WillOnce(Return(true)); std::vector devices; devices.push_back(CreateTestDeviceWithInputAndOutputReports()); EXPECT_CALL(delegate(), RunChooserInternal) .WillOnce(Return(ByMove(std::move(devices)))); EXPECT_EQ(true, EvalJs(shell(), R"((async () => { let devices = await navigator.hid.requestDevice({filters:[]}); return devices instanceof Array && devices.length == 1 && devices[0] instanceof HIDDevice; })())")); } IN_PROC_BROWSER_TEST_F(HidTest, DisallowRequestDevice) { EXPECT_TRUE(NavigateToURL(shell(), GetTestUrl(nullptr, "simple_page.html"))); EXPECT_CALL(delegate(), CanRequestDevicePermission).WillOnce(Return(false)); EXPECT_CALL(delegate(), RunChooserInternal).Times(Exactly(0)); EXPECT_EQ(0, EvalJs(shell(), R"((async () => { let devices = await navigator.hid.requestDevice({filters:[]}); return devices.length; })())")); } IN_PROC_BROWSER_TEST_F(HidTest, ProtectedReportsAreFiltered) { LOG(ERROR) << "HidTest.ProtectedReportsAreFiltered"; EXPECT_TRUE(NavigateToURL(shell(), GetTestUrl(nullptr, "simple_page.html"))); auto device = CreateTestDeviceWithInputAndOutputReports(); // Mark the input report as protected. device->protected_input_report_ids = std::vector{0}; hid_manager()->AddDevice(std::move(device)); EXPECT_CALL(delegate(), HasDevicePermission).WillOnce(Return(true)); EXPECT_EQ(true, EvalJs(shell(), R"((async () => { let devices = await navigator.hid.getDevices(); return devices instanceof Array && devices.length == 1 && devices[0] instanceof HIDDevice && devices[0].collections instanceof Array && devices[0].collections.length == 1 && devices[0].collections[0].inputReports instanceof Array && devices[0].collections[0].inputReports.length == 0 && devices[0].collections[0].outputReports instanceof Array && devices[0].collections[0].outputReports.length == 1; })())")); } IN_PROC_BROWSER_TEST_F(HidTest, DeviceWithAllProtectedReportsIsExcluded) { EXPECT_TRUE(NavigateToURL(shell(), GetTestUrl(nullptr, "simple_page.html"))); auto device = CreateTestDeviceWithInputAndOutputReports(); // Mark both the input and output reports as protected. device->protected_input_report_ids = std::vector{0}; device->protected_output_report_ids = std::vector{0}; hid_manager()->AddDevice(std::move(device)); EXPECT_EQ(true, EvalJs(shell(), R"((async () => { let devices = await navigator.hid.getDevices(); return devices instanceof Array && devices.length == 0; })())")); } class HidFencedFramesBrowserTest : public HidTest { public: HidFencedFramesBrowserTest() = default; HidFencedFramesBrowserTest(const HidFencedFramesBrowserTest&) = delete; HidFencedFramesBrowserTest& operator=(const HidFencedFramesBrowserTest&) = delete; ~HidFencedFramesBrowserTest() override = default; void SetUpOnMainThread() override { HidTest::SetUpOnMainThread(); ASSERT_TRUE(embedded_test_server()->Start()); } content::test::FencedFrameTestHelper& fenced_frame_test_helper() { return fenced_frame_helper_; } WebContents* GetWebContents() { return shell()->web_contents(); } private: content::test::FencedFrameTestHelper fenced_frame_helper_; }; IN_PROC_BROWSER_TEST_F(HidFencedFramesBrowserTest, BlockFromFencedFrame) { EXPECT_TRUE(NavigateToURL( shell(), embedded_test_server()->GetURL("/simple_page.html"))); // Three devices are added but only two will have permission granted. for (int i = 0; i < 3; i++) { auto device = CreateTestDeviceWithInputAndOutputReports(); device->guid = base::StringPrintf("test-guid-%02d", i); hid_manager()->AddDevice(std::move(device)); } EXPECT_CALL(delegate(), HasDevicePermission) .WillOnce(Return(true)) .WillOnce(Return(false)) .WillOnce(Return(true)); EXPECT_EQ( 2, EvalJs(shell(), R"(navigator.hid.getDevices().then(devices => devices.length))")); // Loads a fenced frame const GURL kFencedFrameUrl = embedded_test_server()->GetURL("/fenced_frames/empty.html"); content::RenderFrameHost* render_frame_host = fenced_frame_test_helper().CreateFencedFrame( GetWebContents()->GetPrimaryMainFrame(), kFencedFrameUrl); ASSERT_NE(nullptr, render_frame_host); // Tries to request a device from the fenced frame, which must cause an error. constexpr char kFencedFrameError[] = "Access to the feature \"hid\" is disallowed by permissions policy."; auto result = content::EvalJs( render_frame_host, R"(navigator.hid.getDevices().then(devices => devices.length))"); EXPECT_THAT(result.error, ::testing::HasSubstr(kFencedFrameError)); } } // namespace content