| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/test/test_file_util.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "chrome/browser/bluetooth/web_bluetooth_test_utils.h" |
| #include "chrome/browser/controlled_frame/controlled_frame_permission_request_test_base.h" |
| #include "chrome/browser/serial/serial_chooser_context.h" |
| #include "chrome/browser/serial/serial_chooser_context_factory.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/views/file_system_access/file_system_access_test_utils.h" |
| #include "chrome/browser/usb/usb_browser_test_utils.h" |
| #include "chrome/browser/usb/usb_chooser_context.h" |
| #include "chrome/browser/usb/usb_chooser_context_factory.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/test/bluetooth_test_utils.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "services/device/public/cpp/test/fake_serial_port_manager.h" |
| #include "services/device/public/cpp/test/fake_usb_device_manager.h" |
| #include "services/device/public/mojom/serial.mojom.h" |
| #include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-forward.h" |
| #include "ui/shell_dialogs/select_file_dialog.h" |
| |
| namespace { |
| |
| using testing::Return; |
| |
| constexpr char kSuccessResult[] = "SUCCESS"; |
| constexpr char kFailResult[] = "error"; |
| |
| // USB |
| constexpr char kFakeUsbDeviceSerialNumber[] = "123456"; |
| |
| // Serial |
| constexpr char kExpectedPortsLength[] = "1"; |
| |
| // Bluetooth |
| constexpr char kFakeBluetoothDeviceName[] = "Test Device"; |
| constexpr char kDeviceAddress[] = "00:00:00:00:00:00"; |
| constexpr char kHeartRateUUIDString[] = "0000180d-0000-1000-8000-00805f9b34fb"; |
| |
| } // namespace |
| |
| namespace controlled_frame { |
| |
| class ControlledFrameDisabledPermissionTest |
| : public ControlledFramePermissionRequestTestBase, |
| public testing::WithParamInterface<DisabledPermissionTestParam> {}; |
| |
| class ControlledFrameDisabledPermissionUsbTest |
| : public ControlledFrameDisabledPermissionTest { |
| public: |
| ControlledFrameDisabledPermissionUsbTest() = default; |
| ~ControlledFrameDisabledPermissionUsbTest() override = default; |
| |
| void SetUpOnMainThread() override { |
| ControlledFrameDisabledPermissionTest::SetUpOnMainThread(); |
| fake_device_info_ = device_manager_.CreateAndAddDevice( |
| 0, 0, "Test Manufacturer", "Test Device", kFakeUsbDeviceSerialNumber); |
| mojo::PendingRemote<device::mojom::UsbDeviceManager> device_manager; |
| device_manager_.AddReceiver( |
| device_manager.InitWithNewPipeAndPassReceiver()); |
| UsbChooserContextFactory::GetForProfile(browser()->profile()) |
| ->SetDeviceManagerForTesting(std::move(device_manager)); |
| |
| test_content_browser_client_.SetAsBrowserClient(); |
| } |
| |
| void TearDownOnMainThread() override { |
| test_content_browser_client_.UnsetAsBrowserClient(); |
| ControlledFrameDisabledPermissionTest::TearDownOnMainThread(); |
| } |
| |
| void UseFakeChooser() { |
| test_content_browser_client_.delegate().UseFakeChooser(); |
| } |
| |
| private: |
| device::FakeUsbDeviceManager device_manager_; |
| device::mojom::UsbDeviceInfoPtr fake_device_info_; |
| TestUsbContentBrowserClient test_content_browser_client_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(ControlledFrameDisabledPermissionUsbTest, WebUSB) { |
| UseFakeChooser(); |
| |
| DisabledPermissionTestCase test_case; |
| test_case.request_script = content::JsReplace(R"( |
| (async () => { |
| try { |
| const device = |
| await navigator.usb.requestDevice({ filters: [] }); |
| return device.serialNumber; |
| } catch (_) { |
| return $1; |
| } |
| })(); |
| )", |
| kFailResult); |
| test_case.policy_features.insert( |
| network::mojom::PermissionsPolicyFeature::kUsb); |
| test_case.success_result = kFakeUsbDeviceSerialNumber; |
| test_case.failure_result = kFailResult; |
| |
| DisabledPermissionTestParam test_param = GetParam(); |
| VerifyDisabledPermission(test_case, test_param); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(/*no prefix*/ |
| , |
| ControlledFrameDisabledPermissionUsbTest, |
| testing::ValuesIn( |
| GetDefaultDisabledPermissionTestParams()), |
| [](const testing::TestParamInfo< |
| DisabledPermissionTestParam>& info) { |
| return info.param.name; |
| }); |
| |
| class ControlledFrameDisabledPermissionSerialTest |
| : public ControlledFrameDisabledPermissionTest { |
| public: |
| void SetUpOnMainThread() override { |
| ControlledFrameDisabledPermissionTest::SetUpOnMainThread(); |
| mojo::PendingRemote<device::mojom::SerialPortManager> port_manager; |
| port_manager_.AddReceiver(port_manager.InitWithNewPipeAndPassReceiver()); |
| context()->SetPortManagerForTesting(std::move(port_manager)); |
| } |
| |
| void TearDownOnMainThread() override { |
| ControlledFrameDisabledPermissionTest::TearDownOnMainThread(); |
| } |
| |
| device::FakeSerialPortManager& port_manager() { return port_manager_; } |
| SerialChooserContext* context() { |
| return SerialChooserContextFactory::GetForProfile(browser()->profile()); |
| } |
| |
| void CreatePortAndGrantPermissionToOrigin(const url::Origin& origin) { |
| // Create port and grant permission to it. |
| auto port = device::mojom::SerialPortInfo::New(); |
| port->token = base::UnguessableToken::Create(); |
| context()->GrantPortPermission(origin, *port); |
| port_manager().AddPort(std::move(port)); |
| } |
| |
| private: |
| device::FakeSerialPortManager port_manager_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(ControlledFrameDisabledPermissionSerialTest, WebSerial) { |
| DisabledPermissionTestCase test_case; |
| test_case.request_script = content::JsReplace(R"( |
| (async () => { |
| try { |
| const ports = |
| await navigator.serial.getPorts().then(ports => ports.length); |
| return ports !== 0 ? ports.toString() : $1; |
| } catch (_) { |
| return $1; |
| } |
| })() |
| )", |
| kFailResult); |
| |
| test_case.policy_features.insert( |
| network::mojom::PermissionsPolicyFeature::kSerial); |
| test_case.success_result = kExpectedPortsLength; |
| test_case.failure_result = kFailResult; |
| |
| DisabledPermissionTestParam test_param = GetParam(); |
| auto [app_frame, controlled_frame] = |
| SetUpControlledFrame(test_case, test_param); |
| if (!app_frame || !controlled_frame) { |
| return; |
| } |
| CreatePortAndGrantPermissionToOrigin(app_frame->GetLastCommittedOrigin()); |
| CreatePortAndGrantPermissionToOrigin( |
| controlled_frame->GetLastCommittedOrigin()); |
| VerifyDisabledPermission(test_case, test_param, app_frame, controlled_frame); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(/*no prefix*/ |
| , |
| ControlledFrameDisabledPermissionSerialTest, |
| testing::ValuesIn( |
| GetDefaultDisabledPermissionTestParams()), |
| [](const testing::TestParamInfo< |
| DisabledPermissionTestParam>& info) { |
| return info.param.name; |
| }); |
| |
| class ControlledFrameDisabledPermissionWebBluetoothTest |
| : public ControlledFrameDisabledPermissionTest { |
| public: |
| void SetUpOnMainThread() override { |
| ControlledFrameDisabledPermissionTest::SetUpOnMainThread(); |
| // Hook up the test bluetooth delegate. |
| SetFakeBlueboothAdapter(); |
| old_browser_client_ = content::SetBrowserClientForTesting(&browser_client_); |
| } |
| |
| void TearDownOnMainThread() override { |
| content::SetBrowserClientForTesting(old_browser_client_); |
| ControlledFrameDisabledPermissionTest::TearDownOnMainThread(); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| // Sets up the blink runtime feature for accessing navigator.bluetooth. |
| command_line->AppendSwitch( |
| switches::kEnableExperimentalWebPlatformFeatures); |
| } |
| |
| void SetFakeBlueboothAdapter() { |
| adapter_ = new FakeBluetoothAdapter(); |
| EXPECT_CALL(*adapter_, IsPresent()).WillRepeatedly(Return(true)); |
| EXPECT_CALL(*adapter_, IsPowered()).WillRepeatedly(Return(true)); |
| content::SetBluetoothAdapter(adapter_); |
| } |
| |
| void AddFakeDevice(const std::string& device_address) { |
| const device::BluetoothUUID kHeartRateUUID(kHeartRateUUIDString); |
| auto fake_device = |
| std::make_unique<testing::NiceMock<device::MockBluetoothDevice>>( |
| adapter_.get(), /*bluetooth_class=*/0u, kFakeBluetoothDeviceName, |
| device_address, /*paired=*/true, /*connected=*/true); |
| fake_device->AddUUID(kHeartRateUUID); |
| fake_device->AddMockService( |
| std::make_unique<testing::NiceMock<device::MockBluetoothGattService>>( |
| fake_device.get(), kHeartRateUUIDString, kHeartRateUUID, |
| /*is_primary=*/true)); |
| adapter_->AddMockDevice(std::move(fake_device)); |
| } |
| |
| void SetDeviceToSelect(const std::string& device_address) { |
| browser_client_.bluetooth_delegate()->SetDeviceToSelect(device_address); |
| } |
| |
| private: |
| scoped_refptr<FakeBluetoothAdapter> adapter_; |
| BluetoothTestContentBrowserClient browser_client_; |
| raw_ptr<content::ContentBrowserClient> old_browser_client_ = nullptr; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(ControlledFrameDisabledPermissionWebBluetoothTest, |
| WebBluetooth) { |
| DisabledPermissionTestCase test_case; |
| test_case.request_script = content::JsReplace(R"( |
| (async () => { |
| try { |
| const device = await navigator.bluetooth.requestDevice({ |
| filters: [{services: ['heart_rate']}] |
| }); |
| return device.name; |
| } catch (e) { |
| return $1; |
| } |
| })(); |
| )", |
| kFailResult); |
| test_case.policy_features.insert( |
| network::mojom::PermissionsPolicyFeature::kBluetooth); |
| test_case.success_result = kFakeBluetoothDeviceName; |
| test_case.failure_result = kFailResult; |
| |
| DisabledPermissionTestParam test_param = GetParam(); |
| auto [app_frame, controlled_frame] = |
| SetUpControlledFrame(test_case, test_param); |
| if (!app_frame || !controlled_frame) { |
| return; |
| } |
| |
| AddFakeDevice(kDeviceAddress); |
| SetDeviceToSelect(kDeviceAddress); |
| |
| VerifyDisabledPermission(test_case, test_param, app_frame, controlled_frame); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(/*no prefix*/ |
| , |
| ControlledFrameDisabledPermissionWebBluetoothTest, |
| testing::ValuesIn( |
| GetDefaultDisabledPermissionTestParams()), |
| [](const testing::TestParamInfo< |
| DisabledPermissionTestParam>& info) { |
| return info.param.name; |
| }); |
| |
| class ControlledFrameDisabledPermissionFileSystemAccessTest |
| : public ControlledFrameDisabledPermissionTest { |
| public: |
| void SetUpOnMainThread() override { |
| ASSERT_TRUE( |
| temp_dir_.CreateUniqueTempDirUnderPath(base::GetTempDirForTesting())); |
| ControlledFrameDisabledPermissionTest::SetUpOnMainThread(); |
| } |
| |
| void TearDownOnMainThread() override { |
| ASSERT_TRUE(temp_dir_.Delete()); |
| ControlledFrameDisabledPermissionTest::TearDownOnMainThread(); |
| } |
| |
| base::FilePath CreateTestFile() { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::FilePath result; |
| EXPECT_TRUE(base::CreateTemporaryFileInDir(temp_dir_.GetPath(), &result)); |
| return result; |
| } |
| |
| private: |
| base::ScopedTempDir temp_dir_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(ControlledFrameDisabledPermissionFileSystemAccessTest, |
| FileSystemAccess) { |
| DisabledPermissionTestParam test_param = GetParam(); |
| // Skips permissions-policy tests since FSA does not have a corresponding |
| // policy feature. |
| if (test_param.name == "BothFailsWhenPermissionsPolicyIsNotEnabled") { |
| return; |
| } |
| |
| DisabledPermissionTestCase test_case; |
| test_case.request_script = content::JsReplace(R"( |
| (async function() { |
| try { |
| const [handle] = await showOpenFilePicker({ |
| types: [ |
| { |
| description: 'All files', |
| accept: { |
| '*/*': ['.txt', '.pdf', '.jpg', '.png'], |
| }, |
| }, |
| ], |
| }); |
| await handle.getFile(); |
| return $1; |
| } catch (error) { |
| return $2; |
| } |
| })(); |
| )", |
| kSuccessResult, kFailResult); |
| |
| test_case.success_result = kSuccessResult; |
| test_case.failure_result = kFailResult; |
| |
| const base::FilePath test_file = CreateTestFile(); |
| |
| ui::SelectFileDialog::SetFactory( |
| std::make_unique<SelectPredeterminedFileDialogFactory>( |
| std::vector<base::FilePath>{test_file})); |
| |
| VerifyDisabledPermission(test_case, test_param); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(/*no prefix*/ |
| , |
| ControlledFrameDisabledPermissionFileSystemAccessTest, |
| testing::ValuesIn( |
| GetDefaultDisabledPermissionTestParams()), |
| [](const testing::TestParamInfo< |
| DisabledPermissionTestParam>& info) { |
| return info.param.name; |
| }); |
| |
| } // namespace controlled_frame |