| // Copyright 2019 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 <string> |
| |
| #include "base/macros.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/mock_callback.h" |
| #include "base/test/task_environment.h" |
| #include "chrome/browser/hid/hid_chooser_context.h" |
| #include "chrome/browser/hid/hid_chooser_context_factory.h" |
| #include "chrome/browser/ui/hid/hid_chooser_controller.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chrome/test/base/chrome_render_view_host_test_harness.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "content/public/browser/hid_chooser.h" |
| #include "content/public/test/web_contents_tester.h" |
| #include "services/device/public/cpp/hid/fake_hid_manager.h" |
| #include "services/device/public/mojom/hid.mojom.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| |
| const char kDefaultTestUrl[] = "https://www.google.com/"; |
| |
| const char* const kTestPhysicalDeviceIds[] = {"1", "2", "3"}; |
| |
| const uint16_t kYubicoVendorId = 0x1050; |
| const uint16_t kYubicoGnubbyProductId = 0x0200; |
| |
| class FakeHidChooserView : public ChooserController::View { |
| public: |
| FakeHidChooserView() {} |
| |
| void set_options_initialized_quit_closure(base::OnceClosure quit_closure) { |
| options_initialized_quit_closure_ = std::move(quit_closure); |
| } |
| |
| // ChooserController::View: |
| void OnOptionAdded(size_t index) override {} |
| void OnOptionRemoved(size_t index) override {} |
| void OnOptionsInitialized() override { |
| if (options_initialized_quit_closure_) |
| std::move(options_initialized_quit_closure_).Run(); |
| } |
| void OnOptionUpdated(size_t index) override {} |
| void OnAdapterEnabledChanged(bool enabled) override {} |
| void OnRefreshStateChanged(bool enabled) override {} |
| |
| private: |
| base::OnceClosure options_initialized_quit_closure_; |
| |
| DISALLOW_COPY_AND_ASSIGN(FakeHidChooserView); |
| }; |
| |
| } // namespace |
| |
| class HidChooserControllerTest : public ChromeRenderViewHostTestHarness { |
| public: |
| HidChooserControllerTest() {} |
| |
| void SetUp() override { |
| ChromeRenderViewHostTestHarness::SetUp(); |
| |
| content::WebContentsTester* web_contents_tester = |
| content::WebContentsTester::For(web_contents()); |
| web_contents_tester->NavigateAndCommit(GURL(kDefaultTestUrl)); |
| |
| // Set fake HID manager for HidChooserContext. |
| mojo::PendingRemote<device::mojom::HidManager> hid_manager; |
| hid_manager_.Bind(hid_manager.InitWithNewPipeAndPassReceiver()); |
| HidChooserContextFactory::GetForProfile(profile())->SetHidManagerForTesting( |
| std::move(hid_manager)); |
| } |
| |
| std::unique_ptr<HidChooserController> CreateHidChooserController( |
| std::vector<blink::mojom::HidDeviceFilterPtr> filters, |
| content::HidChooser::Callback callback = base::DoNothing()) { |
| auto hid_chooser_controller = std::make_unique<HidChooserController>( |
| main_rfh(), std::move(filters), std::move(callback)); |
| hid_chooser_controller->set_view(&fake_hid_chooser_view_); |
| return hid_chooser_controller; |
| } |
| |
| device::mojom::HidDeviceInfoPtr CreateAndAddFakeHidDevice( |
| const std::string& physical_device_id, |
| uint32_t vendor_id, |
| uint16_t product_id, |
| const std::string& product_string, |
| const std::string& serial_number, |
| uint16_t usage_page = device::mojom::kPageGenericDesktop, |
| uint16_t usage = device::mojom::kGenericDesktopGamePad) { |
| return hid_manager_.CreateAndAddDeviceWithTopLevelUsage( |
| physical_device_id, vendor_id, product_id, product_string, |
| serial_number, device::mojom::HidBusType::kHIDBusTypeUSB, usage_page, |
| usage); |
| } |
| |
| blink::mojom::DeviceIdFilterPtr CreateVendorFilter(uint16_t vendor_id) { |
| return blink::mojom::DeviceIdFilter::NewVendor(vendor_id); |
| } |
| |
| blink::mojom::DeviceIdFilterPtr CreateVendorAndProductFilter( |
| uint16_t vendor_id, |
| uint16_t product_id) { |
| return blink::mojom::DeviceIdFilter::NewVendorAndProduct( |
| blink::mojom::VendorAndProduct::New(vendor_id, product_id)); |
| } |
| |
| blink::mojom::UsageFilterPtr CreatePageFilter(uint16_t usage_page) { |
| return blink::mojom::UsageFilter::NewPage(usage_page); |
| } |
| |
| blink::mojom::UsageFilterPtr CreateUsageAndPageFilter(uint16_t usage_page, |
| uint16_t usage) { |
| return blink::mojom::UsageFilter::NewUsageAndPage( |
| device::mojom::HidUsageAndPage::New(usage, usage_page)); |
| } |
| |
| protected: |
| device::FakeHidManager hid_manager_; |
| FakeHidChooserView fake_hid_chooser_view_; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(HidChooserControllerTest); |
| }; |
| |
| TEST_F(HidChooserControllerTest, EmptyChooser) { |
| auto hid_chooser_controller = CreateHidChooserController({}); |
| base::RunLoop run_loop; |
| fake_hid_chooser_view_.set_options_initialized_quit_closure( |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| EXPECT_EQ(0u, hid_chooser_controller->NumOptions()); |
| } |
| |
| TEST_F(HidChooserControllerTest, AddBlockedFidoDevice) { |
| // FIDO U2F devices (and other devices on the USB blocklist) should be |
| // excluded from the device chooser. |
| auto hid_chooser_controller = CreateHidChooserController({}); |
| base::RunLoop run_loop; |
| fake_hid_chooser_view_.set_options_initialized_quit_closure( |
| run_loop.QuitClosure()); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], kYubicoVendorId, |
| kYubicoGnubbyProductId, "gnubby", "001"); |
| run_loop.Run(); |
| EXPECT_EQ(0u, hid_chooser_controller->NumOptions()); |
| } |
| |
| TEST_F(HidChooserControllerTest, AddUnknownFidoDevice) { |
| // Devices that expose a top-level collection with the FIDO usage page should |
| // be blocked even if they aren't on the USB blocklist. |
| const uint16_t kFidoU2fHidUsage = 1; |
| auto hid_chooser_controller = CreateHidChooserController({}); |
| base::RunLoop run_loop; |
| fake_hid_chooser_view_.set_options_initialized_quit_closure( |
| run_loop.QuitClosure()); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "fido", "001", |
| device::mojom::kPageFido, kFidoU2fHidUsage); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 2, 2, "fido", "002", |
| device::mojom::kPageFido, 0); |
| run_loop.Run(); |
| EXPECT_EQ(0u, hid_chooser_controller->NumOptions()); |
| } |
| |
| TEST_F(HidChooserControllerTest, AddNamedDevice) { |
| auto hid_chooser_controller = CreateHidChooserController({}); |
| base::RunLoop run_loop; |
| fake_hid_chooser_view_.set_options_initialized_quit_closure( |
| run_loop.QuitClosure()); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001"); |
| run_loop.Run(); |
| EXPECT_EQ(1u, hid_chooser_controller->NumOptions()); |
| EXPECT_EQ(base::ASCIIToUTF16("a (Vendor: 0x0001, Product: 0x0001)"), |
| hid_chooser_controller->GetOption(0)); |
| } |
| |
| TEST_F(HidChooserControllerTest, AddUnnamedDevice) { |
| auto hid_chooser_controller = CreateHidChooserController({}); |
| base::RunLoop run_loop; |
| fake_hid_chooser_view_.set_options_initialized_quit_closure( |
| run_loop.QuitClosure()); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "", "001"); |
| run_loop.Run(); |
| EXPECT_EQ(1u, hid_chooser_controller->NumOptions()); |
| EXPECT_EQ( |
| base::ASCIIToUTF16("Unknown Device (Vendor: 0x0001, Product: 0x0001)"), |
| hid_chooser_controller->GetOption(0)); |
| } |
| |
| TEST_F(HidChooserControllerTest, DeviceIdFilterVendorOnly) { |
| std::vector<blink::mojom::HidDeviceFilterPtr> filters; |
| filters.push_back( |
| blink::mojom::HidDeviceFilter::New(CreateVendorFilter(1), nullptr)); |
| auto hid_chooser_controller = CreateHidChooserController(std::move(filters)); |
| |
| base::RunLoop run_loop; |
| fake_hid_chooser_view_.set_options_initialized_quit_closure( |
| run_loop.QuitClosure()); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001"); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 1, 2, "b", "002"); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 2, 2, "c", "003"); |
| run_loop.Run(); |
| |
| EXPECT_EQ(2u, hid_chooser_controller->NumOptions()); |
| |
| std::set<base::string16> options; |
| options.insert(hid_chooser_controller->GetOption(0)); |
| options.insert(hid_chooser_controller->GetOption(1)); |
| EXPECT_EQ(1u, options.count( |
| base::ASCIIToUTF16("a (Vendor: 0x0001, Product: 0x0001)"))); |
| EXPECT_EQ(1u, options.count( |
| base::ASCIIToUTF16("b (Vendor: 0x0001, Product: 0x0002)"))); |
| } |
| |
| TEST_F(HidChooserControllerTest, DeviceIdFilterVendorAndProduct) { |
| std::vector<blink::mojom::HidDeviceFilterPtr> filters; |
| filters.push_back(blink::mojom::HidDeviceFilter::New( |
| CreateVendorAndProductFilter(1, 1), nullptr)); |
| auto hid_chooser_controller = CreateHidChooserController(std::move(filters)); |
| |
| base::RunLoop run_loop; |
| fake_hid_chooser_view_.set_options_initialized_quit_closure( |
| run_loop.QuitClosure()); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001"); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 1, 2, "b", "002"); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 2, 2, "c", "003"); |
| run_loop.Run(); |
| |
| EXPECT_EQ(1u, hid_chooser_controller->NumOptions()); |
| EXPECT_EQ(base::ASCIIToUTF16("a (Vendor: 0x0001, Product: 0x0001)"), |
| hid_chooser_controller->GetOption(0)); |
| } |
| |
| TEST_F(HidChooserControllerTest, UsageFilterUsagePageOnly) { |
| std::vector<blink::mojom::HidDeviceFilterPtr> filters; |
| filters.push_back(blink::mojom::HidDeviceFilter::New( |
| nullptr, CreatePageFilter(device::mojom::kPageGenericDesktop))); |
| auto hid_chooser_controller = CreateHidChooserController(std::move(filters)); |
| |
| base::RunLoop run_loop; |
| fake_hid_chooser_view_.set_options_initialized_quit_closure( |
| run_loop.QuitClosure()); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001", |
| device::mojom::kPageGenericDesktop, |
| device::mojom::kGenericDesktopGamePad); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 2, 2, "b", "002", |
| device::mojom::kPageSimulation, 5); |
| run_loop.Run(); |
| |
| EXPECT_EQ(1u, hid_chooser_controller->NumOptions()); |
| EXPECT_EQ(base::ASCIIToUTF16("a (Vendor: 0x0001, Product: 0x0001)"), |
| hid_chooser_controller->GetOption(0)); |
| } |
| |
| TEST_F(HidChooserControllerTest, UsageFilterUsageAndPage) { |
| std::vector<blink::mojom::HidDeviceFilterPtr> filters; |
| filters.push_back(blink::mojom::HidDeviceFilter::New( |
| nullptr, |
| CreateUsageAndPageFilter(device::mojom::kPageGenericDesktop, |
| device::mojom::kGenericDesktopGamePad))); |
| auto hid_chooser_controller = CreateHidChooserController(std::move(filters)); |
| |
| base::RunLoop run_loop; |
| fake_hid_chooser_view_.set_options_initialized_quit_closure( |
| run_loop.QuitClosure()); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001", |
| device::mojom::kPageGenericDesktop, |
| device::mojom::kGenericDesktopGamePad); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 2, 2, "b", "002", |
| device::mojom::kPageGenericDesktop, |
| device::mojom::kGenericDesktopKeyboard); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 3, 3, "c", "003", |
| device::mojom::kPageSimulation, 5); |
| run_loop.Run(); |
| |
| EXPECT_EQ(1u, hid_chooser_controller->NumOptions()); |
| EXPECT_EQ(base::ASCIIToUTF16("a (Vendor: 0x0001, Product: 0x0001)"), |
| hid_chooser_controller->GetOption(0)); |
| } |
| |
| TEST_F(HidChooserControllerTest, DeviceIdAndUsageFilterIntersection) { |
| std::vector<blink::mojom::HidDeviceFilterPtr> filters; |
| filters.push_back(blink::mojom::HidDeviceFilter::New( |
| CreateVendorAndProductFilter(1, 1), |
| CreateUsageAndPageFilter(device::mojom::kPageGenericDesktop, |
| device::mojom::kGenericDesktopGamePad))); |
| auto hid_chooser_controller = CreateHidChooserController(std::move(filters)); |
| |
| base::RunLoop run_loop; |
| fake_hid_chooser_view_.set_options_initialized_quit_closure( |
| run_loop.QuitClosure()); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001", |
| device::mojom::kPageGenericDesktop, |
| device::mojom::kGenericDesktopGamePad); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 2, 2, "b", "002", |
| device::mojom::kPageGenericDesktop, |
| device::mojom::kGenericDesktopGamePad); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 1, 1, "c", "003", |
| device::mojom::kPageSimulation, 5); |
| run_loop.Run(); |
| |
| EXPECT_EQ(1u, hid_chooser_controller->NumOptions()); |
| EXPECT_EQ(base::ASCIIToUTF16("a (Vendor: 0x0001, Product: 0x0001)"), |
| hid_chooser_controller->GetOption(0)); |
| } |
| |
| TEST_F(HidChooserControllerTest, DeviceIdAndUsageFilterUnion) { |
| std::vector<blink::mojom::HidDeviceFilterPtr> filters; |
| filters.push_back(blink::mojom::HidDeviceFilter::New( |
| CreateVendorAndProductFilter(1, 1), nullptr)); |
| filters.push_back(blink::mojom::HidDeviceFilter::New( |
| nullptr, |
| CreateUsageAndPageFilter(device::mojom::kPageGenericDesktop, |
| device::mojom::kGenericDesktopGamePad))); |
| auto hid_chooser_controller = CreateHidChooserController(std::move(filters)); |
| |
| base::RunLoop run_loop; |
| fake_hid_chooser_view_.set_options_initialized_quit_closure( |
| run_loop.QuitClosure()); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001", |
| device::mojom::kPageGenericDesktop, |
| device::mojom::kGenericDesktopGamePad); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 2, 2, "b", "002", |
| device::mojom::kPageGenericDesktop, |
| device::mojom::kGenericDesktopGamePad); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 1, 1, "c", "003", |
| device::mojom::kPageSimulation, 5); |
| run_loop.Run(); |
| |
| EXPECT_EQ(3u, hid_chooser_controller->NumOptions()); |
| } |
| |
| TEST_F(HidChooserControllerTest, OneItemForSamePhysicalDevice) { |
| base::MockCallback<content::HidChooser::Callback> callback; |
| auto hid_chooser_controller = CreateHidChooserController({}, callback.Get()); |
| |
| base::RunLoop run_loop; |
| fake_hid_chooser_view_.set_options_initialized_quit_closure( |
| run_loop.QuitClosure()); |
| |
| // These two devices have the same physical device ID and should be coalesced |
| // into a single chooser item. |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001", |
| device::mojom::kPageGenericDesktop, |
| device::mojom::kGenericDesktopGamePad); |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001", |
| device::mojom::kPageSimulation, 5); |
| |
| // This device has the same info as the first device except for the physical |
| // device ID. It should have a separate chooser item. |
| CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 1, 1, "a", "001", |
| device::mojom::kPageGenericDesktop, |
| device::mojom::kGenericDesktopGamePad); |
| |
| run_loop.Run(); |
| |
| EXPECT_EQ(2u, hid_chooser_controller->NumOptions()); |
| EXPECT_EQ(base::ASCIIToUTF16("a (Vendor: 0x0001, Product: 0x0001)"), |
| hid_chooser_controller->GetOption(0)); |
| EXPECT_EQ(base::ASCIIToUTF16("a (Vendor: 0x0001, Product: 0x0001)"), |
| hid_chooser_controller->GetOption(1)); |
| |
| EXPECT_CALL(callback, Run(testing::_)) |
| .WillOnce(testing::Invoke( |
| [&](std::vector<device::mojom::HidDeviceInfoPtr> devices) { |
| EXPECT_EQ(2u, devices.size()); |
| EXPECT_EQ(kTestPhysicalDeviceIds[0], |
| devices[0]->physical_device_id); |
| EXPECT_EQ(kTestPhysicalDeviceIds[0], |
| devices[1]->physical_device_id); |
| EXPECT_NE(devices[0]->guid, devices[1]->guid); |
| |
| // Regression test for https://crbug.com/1069057. Ensure that the |
| // set of options is still valid after the callback is run. |
| EXPECT_EQ(2u, hid_chooser_controller->NumOptions()); |
| EXPECT_EQ(base::ASCIIToUTF16("a (Vendor: 0x0001, Product: 0x0001)"), |
| hid_chooser_controller->GetOption(0)); |
| })); |
| hid_chooser_controller->Select({0}); |
| } |
| |
| TEST_F(HidChooserControllerTest, NoMergeWithEmptyPhysicalDeviceId) { |
| auto hid_chooser_controller = CreateHidChooserController({}); |
| |
| base::RunLoop run_loop; |
| fake_hid_chooser_view_.set_options_initialized_quit_closure( |
| run_loop.QuitClosure()); |
| |
| // These two devices have an empty string for the physical device ID and |
| // should not be coalesced. |
| CreateAndAddFakeHidDevice("", 1, 1, "a", "001", |
| device::mojom::kPageGenericDesktop, |
| device::mojom::kGenericDesktopGamePad); |
| CreateAndAddFakeHidDevice("", 1, 1, "a", "001", |
| device::mojom::kPageSimulation, 5); |
| |
| run_loop.Run(); |
| |
| EXPECT_EQ(2u, hid_chooser_controller->NumOptions()); |
| } |