#include <optional>
#include "base/command_line.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/field_trial_params.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/with_feature_override.h"
#include "chrome/browser/bluetooth/bluetooth_chooser_context_factory.h"
#include "chrome/browser/bluetooth/chrome_bluetooth_delegate_impl_client.h"
#include "chrome/browser/bluetooth/web_bluetooth_test_utils.h"
#include "chrome/browser/chrome_content_browser_client.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_dialogs.h"
#include "chrome/browser/ui/chooser_bubble_testapi.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/permissions/bluetooth_delegate_impl.h"
#include "components/permissions/content_setting_permission_context_base.h"
#include "components/permissions/contexts/bluetooth_chooser_context.h"
#include "components/variations/variations_associated_data.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.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_base.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/url_loader_interceptor.h"
#include "device/bluetooth/bluetooth_adapter.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/bluetooth_device.h"
#include "device/bluetooth/bluetooth_gatt_characteristic.h"
#include "device/bluetooth/bluetooth_gatt_connection.h"
#include "device/bluetooth/bluetooth_gatt_notify_session.h"
#include "device/bluetooth/bluetooth_remote_gatt_characteristic.h"
#include "device/bluetooth/bluetooth_remote_gatt_service.h"
#include "device/bluetooth/public/cpp/bluetooth_uuid.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/bluetooth/test/mock_bluetooth_device.h"
#include "device/bluetooth/test/mock_bluetooth_gatt_connection.h"
#include "device/bluetooth/test/mock_bluetooth_gatt_notify_session.h"
#include "device/bluetooth/test/mock_bluetooth_gatt_service.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/bluetooth/web_bluetooth_device_id.h"
namespace {
using ::device::BluetoothGattCharacteristic;
constexpr char kDeviceAddress[] = "00:00:00:00:00:00";
constexpr char kDeviceAddress2[] = "00:00:00:00:00:01";
constexpr char kHeartRateUUIDString[] = "0000180d-0000-1000-8000-00805f9b34fb";
constexpr char kHeartRateMeasurementUUIDString[] =
"00001234-0000-1000-8000-00805f9b34fb";
const device::BluetoothUUID kHeartRateUUID(kHeartRateUUIDString);
const device::BluetoothUUID kHeartRateMeasurementUUID(
kHeartRateMeasurementUUIDString);
constexpr char kExampleUrl[] = "https://example.com";
class WebBluetoothTest : public InProcessBrowserTest {
public:
WebBluetoothTest() = default;
~WebBluetoothTest() override = default;
WebBluetoothTest(const WebBluetoothTest&) = delete;
WebBluetoothTest& operator=(const WebBluetoothTest&) = delete;
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
mock_cert_verifier_.SetUpCommandLine(command_line);
}
void SetUpInProcessBrowserTestFixture() override {
mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
}
void TearDownInProcessBrowserTestFixture() override {
mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
}
void SetUpOnMainThread() override {
mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
host_resolver()->AddRule("*", "127.0.0.1");
url_loader_interceptor_ =
std::make_unique<content::URLLoaderInterceptor>(base::BindRepeating(
[](content::URLLoaderInterceptor::RequestParams* params) {
if (params->url_request.url.GetHost() == "example.com") {
content::URLLoaderInterceptor::WriteResponse(
"content/test/data/simple_page.html", params->client.get());
return true;
}
return false;
}));
adapter_ = base::MakeRefCounted<FakeBluetoothAdapter>();
global_values_ =
device::BluetoothAdapterFactory::Get()->InitGlobalOverrideValues();
global_values_->SetLESupported(true);
device::BluetoothAdapterFactory::SetAdapterForTesting(adapter_);
old_browser_client_ = content::SetBrowserClientForTesting(&browser_client_);
}
void TearDownOnMainThread() override {
content::SetBrowserClientForTesting(old_browser_client_);
url_loader_interceptor_.reset();
}
net::EmbeddedTestServer* CreateHttpsServer() {
https_server_ = std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTPS);
https_server_->ServeFilesFromSourceDirectory("content/test/data");
https_server_->AddDefaultHandlers();
https_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_OK);
return https_server();
}
net::EmbeddedTestServer* https_server() { return https_server_.get(); }
void AddFakeDevice(const std::string& device_address) {
constexpr int kProperties = BluetoothGattCharacteristic::PROPERTY_READ |
BluetoothGattCharacteristic::PROPERTY_NOTIFY;
constexpr int kPermissions = BluetoothGattCharacteristic::PERMISSION_READ;
auto fake_device =
std::make_unique<FakeBluetoothDevice>(adapter_.get(), device_address);
fake_device->AddUUID(kHeartRateUUID);
auto fake_service =
std::make_unique<testing::NiceMock<device::MockBluetoothGattService>>(
fake_device.get(), kHeartRateUUIDString, kHeartRateUUID,
true);
auto fake_characteristic =
std::make_unique<FakeBluetoothGattCharacteristic>(
fake_service.get(), kHeartRateMeasurementUUIDString,
kHeartRateMeasurementUUID, kProperties, kPermissions);
characteristic_ = fake_characteristic.get();
fake_service->AddMockCharacteristic(std::move(fake_characteristic));
fake_device->AddMockService(std::move(fake_service));
adapter_->AddMockDevice(std::move(fake_device));
}
void RemoveFakeDevice(const std::string& device_address) {
adapter_->RemoveMockDevice(device_address);
}
void SimulateDeviceAdvertisement(const std::string& device_address) {
adapter_->SimulateDeviceAdvertisementReceived(device_address);
}
void SetDeviceToSelect(const std::string& device_address) {
browser_client_.bluetooth_delegate()->SetDeviceToSelect(device_address);
}
void UseRealChooser() {
browser_client_.bluetooth_delegate()->UseRealChooser();
}
void CheckLastCommitedOrigin(const std::string& pattern) {
EXPECT_THAT(web_contents_->GetPrimaryMainFrame()
->GetLastCommittedOrigin()
.Serialize(),
testing::StartsWith(pattern));
}
std::unique_ptr<device::BluetoothAdapterFactory::GlobalOverrideValues>
global_values_;
scoped_refptr<FakeBluetoothAdapter> adapter_;
BluetoothTestContentBrowserClient browser_client_;
raw_ptr<content::ContentBrowserClient, AcrossTasksDanglingUntriaged>
old_browser_client_ = nullptr;
raw_ptr<FakeBluetoothGattCharacteristic, AcrossTasksDanglingUntriaged>
characteristic_ = nullptr;
raw_ptr<content::WebContents, AcrossTasksDanglingUntriaged> web_contents_ =
nullptr;
std::unique_ptr<content::URLLoaderInterceptor> url_loader_interceptor_;
std::unique_ptr<net::EmbeddedTestServer> https_server_;
content::ContentMockCertVerifier mock_cert_verifier_;
};
IN_PROC_BROWSER_TEST_F(WebBluetoothTest, WebBluetoothAfterCrash) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
adapter_->SetIsPresent(false);
EXPECT_EQ(
"NotFoundError: Bluetooth adapter not available.",
content::EvalJs(
web_contents_.get(),
"navigator.bluetooth.requestDevice({filters: [{services: [0x180d]}]})"
" .catch(e => e.toString());"));
content::RenderProcessHost* process =
web_contents_->GetPrimaryMainFrame()->GetProcess();
content::RenderProcessHostWatcher crash_observer(
process, content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
process->Shutdown(0);
crash_observer.Wait();
chrome::Reload(browser(), WindowOpenDisposition::CURRENT_TAB);
EXPECT_TRUE(content::WaitForLoadStop(
browser()->tab_strip_model()->GetActiveWebContents()));
EXPECT_EQ(
"NotFoundError: Bluetooth adapter not available.",
content::EvalJs(
web_contents_.get(),
"navigator.bluetooth.requestDevice({filters: [{services: [0x180d]}]})"
" .catch(e => e.toString());"));
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTest, KillSwitchShouldBlock) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
std::map<std::string, std::string> params;
params["Bluetooth"] = permissions::ContentSettingPermissionContextBase::
kPermissionsKillSwitchBlockedValue;
base::AssociateFieldTrialParams(
permissions::ContentSettingPermissionContextBase::
kPermissionsKillSwitchFieldStudy,
"TestGroup", params);
base::FieldTrialList::CreateFieldTrial(
permissions::ContentSettingPermissionContextBase::
kPermissionsKillSwitchFieldStudy,
"TestGroup");
std::string rejection =
content::EvalJs(
web_contents_.get(),
"navigator.bluetooth.requestDevice({filters: [{name: 'Hello'}]})"
" .then(() => 'Success',"
" reason => reason.name + ': ' + reason.message"
" );")
.ExtractString();
EXPECT_THAT(rejection,
testing::MatchesRegex("NotFoundError: .*globally disabled.*"));
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTest, BlocklistShouldBlock) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
if (base::FieldTrialList::TrialExists("WebBluetoothBlocklist")) {
LOG(INFO) << "WebBluetoothBlocklist field trial already configured.";
ASSERT_NE(base::GetFieldTrialParamValue("WebBluetoothBlocklist",
"blocklist_additions")
.find("ed5f25a4"),
std::string::npos)
<< "ERROR: WebBluetoothBlocklist field trial being tested in\n"
"testing/variations/fieldtrial_testing_config_*.json must\n"
"include this test's random UUID 'ed5f25a4' in\n"
"blocklist_additions.\n";
} else {
LOG(INFO) << "Creating WebBluetoothBlocklist field trial for test.";
std::map<std::string, std::string> params;
params["blocklist_additions"] = "ed5f25a4:e";
base::AssociateFieldTrialParams("WebBluetoothBlocklist", "TestGroup",
params);
base::FieldTrialList::CreateFieldTrial("WebBluetoothBlocklist",
"TestGroup");
}
std::string rejection =
content::EvalJs(web_contents_.get(),
"navigator.bluetooth.requestDevice({filters: [{services: "
"[0xed5f25a4]}]})"
" .then(() => 'Success',"
" reason => reason.name + ': ' + reason.message"
" );")
.ExtractString();
EXPECT_THAT(rejection,
testing::MatchesRegex("SecurityError: .*blocklisted UUID.*"));
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTest, NavigateWithChooserCrossOrigin) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
UseRealChooser();
content::TestNavigationObserver observer(
web_contents_, 1 ,
content::MessageLoopRunner::QuitMode::DEFERRED);
auto waiter = test::ChooserBubbleUiWaiter::Create();
EXPECT_TRUE(content::ExecJs(
web_contents_.get(),
"navigator.bluetooth.requestDevice({filters: [{name: 'Hello'}]})",
content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
waiter->WaitForChange();
EXPECT_TRUE(waiter->has_shown());
ASSERT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL("https://google.com")));
observer.Wait();
waiter->WaitForChange();
EXPECT_TRUE(waiter->has_closed());
EXPECT_EQ(GURL("https://google.com"), web_contents_->GetLastCommittedURL());
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTest, ShowChooserInBackgroundTab) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
UseRealChooser();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL(kExampleUrl), WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
EXPECT_EQ("NotFoundError: User cancelled the requestDevice() chooser.",
content::EvalJs(web_contents,
R"((async () => {
try {
await navigator.bluetooth.requestDevice({ filters: [{name: 'Hello'}] });
return "Expected error, got success.";
} catch (e) {
return `${e.name}: ${e.message}`;
}
})())"));
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTest, NotificationStartValueChangeRead) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
AddFakeDevice(kDeviceAddress);
ASSERT_TRUE(characteristic_);
characteristic_->DeferReadUntilNotificationStart();
SetDeviceToSelect(kDeviceAddress);
auto js_values = content::EvalJs(web_contents_.get(), R"((async () => {
const kHeartRateMeasurementUUID = '00001234-0000-1000-8000-00805f9b34fb';
const device = await navigator.bluetooth.requestDevice(
{filters: [{name: 'Test Device', services: ['heart_rate']}]});
const gatt = await device.gatt.connect();
const service = await gatt.getPrimaryService('heart_rate');
const characteristic =
await service.getCharacteristic(kHeartRateMeasurementUUID);
const readPromise = (async () => {
const dataview = await characteristic.readValue();
return dataview.getUint8(0);
})();
const notifyCharacteristic = await characteristic.startNotifications();
const notifyPromise = new Promise(resolve => {
notifyCharacteristic.addEventListener(
'characteristicvaluechanged', event => {
resolve(event.target.value.getUint8(0));
});
});
return Promise.all([readPromise, notifyPromise]);
})())");
const base::Value::List& promise_values = js_values.ExtractList();
EXPECT_EQ(2U, promise_values.size());
EXPECT_EQ(content::ListValueOf(1, 1), js_values);
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTest, NotificationStartValueChangeNotify) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
AddFakeDevice(kDeviceAddress);
ASSERT_TRUE(characteristic_);
characteristic_->EmitChangeNotificationAtNotificationStart();
SetDeviceToSelect(kDeviceAddress);
EXPECT_EQ(1, content::EvalJs(web_contents_.get(), R"((async () => {
const kHeartRateMeasurementUUID = '00001234-0000-1000-8000-00805f9b34fb';
const device = await navigator.bluetooth.requestDevice(
{filters: [{name: 'Test Device', services: ['heart_rate']}]});
const gatt = await device.gatt.connect();
const service = await gatt.getPrimaryService('heart_rate');
const characteristic =
await service.getCharacteristic(kHeartRateMeasurementUUID);
const notifyCharacteristic = await characteristic.startNotifications();
return new Promise((resolve) => {
notifyCharacteristic.addEventListener(
'characteristicvaluechanged', event => {
const value = event.target.value.getUint8(0);
resolve(value);
});
});
})())"));
}
class WebBluetoothPermissionsPolicyTest
: public base::test::WithFeatureOverride,
public WebBluetoothTest {
public:
WebBluetoothPermissionsPolicyTest()
: base::test::WithFeatureOverride(
features::kWebBluetoothNewPermissionsBackend) {}
void SetUpCommandLine(base::CommandLine* command_line) override {
WebBluetoothTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
"WebBluetoothGetDevices");
}
content::EvalJsResult InvokeRequestDevice(
const content::ToRenderFrameHost& adapter) {
return content::EvalJs(adapter, R"((async () => {
try {
const device = await navigator.bluetooth.requestDevice(
{filters: [{name: 'Test Device', services: ['heart_rate']}]});
return [ device.id ];
} catch(e) {
return `${e.name}: ${e.message}`;
}
})())");
}
content::EvalJsResult InvokeGetDevices(
const content::ToRenderFrameHost& adapter) {
return content::EvalJs(adapter, R"((async () => {
try {
const devices = await navigator.bluetooth.getDevices(
{filters: [{name: 'Test Device', services: ['heart_rate']}]});
return devices.map(device => device.id);
} catch(e) {
return `${e.name}: ${e.message}`;
}
})())");
}
};
INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(WebBluetoothPermissionsPolicyTest);
IN_PROC_BROWSER_TEST_P(WebBluetoothPermissionsPolicyTest,
ThrowSecurityWhenIFrameIsDisallowed) {
ASSERT_TRUE(CreateHttpsServer()->Start());
AddFakeDevice(kDeviceAddress);
SetDeviceToSelect(kDeviceAddress);
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
GURL outer_url = https_server()->GetURL(
"outer.com", "/cross_site_iframe_factory.html?outer(inner())");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), outer_url));
content::EvalJsResult inner_device_error = InvokeGetDevices(
content::ChildFrameAt(web_contents_->GetPrimaryMainFrame(), 0));
EXPECT_EQ(
"SecurityError: Failed to execute 'getDevices' on 'Bluetooth': Access to "
"the feature \"bluetooth\" is disallowed by permissions policy.",
inner_device_error);
}
IN_PROC_BROWSER_TEST_P(WebBluetoothPermissionsPolicyTest,
AllowedChildFrameShouldHaveAccessToParentDevices) {
ASSERT_TRUE(CreateHttpsServer()->Start());
AddFakeDevice(kDeviceAddress);
SetDeviceToSelect(kDeviceAddress);
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
GURL outer_url = https_server()->GetURL(
"outer.com",
"/cross_site_iframe_factory.html?outer(inner{allow-bluetooth}())");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), outer_url));
content::EvalJsResult outer_device_id =
InvokeRequestDevice(web_contents_.get());
content::EvalJsResult inner_device_id = InvokeGetDevices(
content::ChildFrameAt(web_contents_->GetPrimaryMainFrame(), 0));
EXPECT_EQ(outer_device_id.ExtractList(), inner_device_id.ExtractList());
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
GURL inner_url = https_server()->GetURL("inner.com", "/simple_page.html");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), inner_url));
content::EvalJsResult inner_device_id_after_navigation =
InvokeGetDevices(web_contents_.get());
EXPECT_EQ(base::Value(base::Value::List()), inner_device_id_after_navigation);
}
IN_PROC_BROWSER_TEST_P(WebBluetoothPermissionsPolicyTest,
ParentShouldHaveAccessToAllowedChildFrameDevices) {
ASSERT_TRUE(CreateHttpsServer()->Start());
AddFakeDevice(kDeviceAddress);
SetDeviceToSelect(kDeviceAddress);
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
GURL outer_url = https_server()->GetURL(
"outer.com",
"/cross_site_iframe_factory.html?outer(inner{allow-bluetooth}())");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), outer_url));
content::EvalJsResult inner_device_id = InvokeRequestDevice(
content::ChildFrameAt(web_contents_->GetPrimaryMainFrame(), 0));
content::EvalJsResult outer_device_id = InvokeGetDevices(web_contents_.get());
EXPECT_EQ(outer_device_id.ExtractList(), inner_device_id.ExtractList());
}
class WebBluetoothTestWithNewPermissionsBackendEnabled
: public WebBluetoothTest {
public:
WebBluetoothTestWithNewPermissionsBackendEnabled() {
feature_list_.InitAndEnableFeature(
features::kWebBluetoothNewPermissionsBackend);
}
WebBluetoothTestWithNewPermissionsBackendEnabled(
const WebBluetoothTestWithNewPermissionsBackendEnabled&) = delete;
WebBluetoothTestWithNewPermissionsBackendEnabled& operator=(
const WebBluetoothTestWithNewPermissionsBackendEnabled&) = delete;
void SetUp() override {
content::IgnoreBluetoothVisibilityRequirementsForTesting();
WebBluetoothTest::SetUp();
}
protected:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(WebBluetoothTestWithNewPermissionsBackendEnabled,
PRE_WebBluetoothPersistentIds) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
AddFakeDevice(kDeviceAddress);
SetDeviceToSelect(kDeviceAddress);
auto request_device_result =
content::EvalJs(web_contents_.get(), R"((async() => {
try {
let device = await navigator.bluetooth.requestDevice({
filters: [{name: 'Test Device'}]});
localStorage.setItem('requestDeviceId', device.id);
return device.id;
} catch(e) {
return `${e.name}: ${e.message}`;
}
})())");
const std::string& granted_id = request_device_result.ExtractString();
EXPECT_TRUE(blink::WebBluetoothDeviceId::IsValid(granted_id));
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTestWithNewPermissionsBackendEnabled,
WebBluetoothPersistentIds) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
AddFakeDevice(kDeviceAddress);
SetDeviceToSelect(kDeviceAddress);
auto request_device_result =
content::EvalJs(web_contents_.get(), R"((async() => {
try {
let device = await navigator.bluetooth.requestDevice({
filters: [{name: 'Test Device'}]});
return device.id;
} catch(e) {
return `${e.name}: ${e.message}`;
}
})())");
const std::string& granted_id = request_device_result.ExtractString();
EXPECT_TRUE(blink::WebBluetoothDeviceId::IsValid(granted_id));
auto local_storage_get_item_result = content::EvalJs(web_contents_.get(), R"(
(async() => {
return localStorage.getItem('requestDeviceId');
})())");
const std::string& prev_granted_id =
local_storage_get_item_result.ExtractString();
EXPECT_EQ(granted_id, prev_granted_id);
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTestWithNewPermissionsBackendEnabled,
PRE_WebBluetoothScanningIdsNotPersistent) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
ASSERT_TRUE(content::ExecJs(web_contents_.get(), R"(
var requestLEScanPromise = navigator.bluetooth.requestLEScan({
acceptAllAdvertisements: true});
)"));
ASSERT_TRUE(content::ExecJs(web_contents_.get(), "requestLEScanPromise"));
ASSERT_TRUE(content::ExecJs(web_contents_.get(), R"(
var advertisementreceivedPromise = new Promise(resolve => {
navigator.bluetooth.addEventListener('advertisementreceived',
event => {
localStorage.setItem('requestLEScanId', event.device.id);
resolve(event.device.id);
});
});
)"));
SimulateDeviceAdvertisement(kDeviceAddress);
auto advertisementreceived_promise_result =
content::EvalJs(web_contents_.get(), "advertisementreceivedPromise ");
const std::string& scan_id =
advertisementreceived_promise_result.ExtractString();
EXPECT_TRUE(blink::WebBluetoothDeviceId::IsValid(scan_id));
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTestWithNewPermissionsBackendEnabled,
WebBluetoothScanningIdsNotPersistent) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
ASSERT_TRUE(content::ExecJs(web_contents_.get(), R"(
var requestLEScanPromise = navigator.bluetooth.requestLEScan({
acceptAllAdvertisements: true});
)"));
ASSERT_TRUE(content::ExecJs(web_contents_.get(), "requestLEScanPromise"));
ASSERT_TRUE(content::ExecJs(web_contents_.get(), R"(
var advertisementreceivedPromise = new Promise(resolve => {
navigator.bluetooth.addEventListener('advertisementreceived',
event => {
resolve(event.device.id);
});
});
)"));
SimulateDeviceAdvertisement(kDeviceAddress);
auto advertisementreceived_promise_result =
content::EvalJs(web_contents_.get(), "advertisementreceivedPromise ");
const std::string& scan_id =
advertisementreceived_promise_result.ExtractString();
EXPECT_TRUE(blink::WebBluetoothDeviceId::IsValid(scan_id));
auto local_storage_get_item_result = content::EvalJs(
web_contents_.get(), "localStorage.getItem('requestLEScanId')");
const std::string& prev_scan_id =
local_storage_get_item_result.ExtractString();
EXPECT_NE(scan_id, prev_scan_id);
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTestWithNewPermissionsBackendEnabled,
PRE_WebBluetoothIdsUsedInWebBluetoothScanning) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
AddFakeDevice(kDeviceAddress);
SetDeviceToSelect(kDeviceAddress);
auto request_device_result =
content::EvalJs(web_contents_.get(), R"((async() => {
try {
let device = await navigator.bluetooth.requestDevice({
filters: [{name: 'Test Device'}]});
localStorage.setItem('requestDeviceId', device.id);
return device.id;
} catch(e) {
return `${e.name}: ${e.message}`;
}
})())");
const std::string& granted_id = request_device_result.ExtractString();
EXPECT_TRUE(blink::WebBluetoothDeviceId::IsValid(granted_id));
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTestWithNewPermissionsBackendEnabled,
WebBluetoothIdsUsedInWebBluetoothScanning) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
ASSERT_TRUE(content::ExecJs(web_contents_.get(), R"(
var requestLEScanPromise = navigator.bluetooth.requestLEScan({
acceptAllAdvertisements: true});
)"));
ASSERT_TRUE(content::ExecJs(web_contents_.get(), "requestLEScanPromise"));
ASSERT_TRUE(content::ExecJs(web_contents_.get(), R"(
var advertisementreceivedPromise = new Promise(resolve => {
navigator.bluetooth.addEventListener('advertisementreceived',
event => {
resolve(event.device.id);
});
});
)"));
SimulateDeviceAdvertisement(kDeviceAddress);
auto advertisementreceived_promise_result =
content::EvalJs(web_contents_.get(), "advertisementreceivedPromise ");
const std::string& scan_id =
advertisementreceived_promise_result.ExtractString();
EXPECT_TRUE(blink::WebBluetoothDeviceId::IsValid(scan_id));
auto local_storage_get_item_result = content::EvalJs(
web_contents_.get(), "localStorage.getItem('requestDeviceId')");
const std::string& granted_id = local_storage_get_item_result.ExtractString();
EXPECT_EQ(scan_id, granted_id);
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTestWithNewPermissionsBackendEnabled,
PRE_WebBluetoothPersistentServices) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
AddFakeDevice(kDeviceAddress);
SetDeviceToSelect(kDeviceAddress);
EXPECT_EQ(kHeartRateUUIDString,
content::EvalJs(web_contents_.get(), R"((async() => {
try {
let device = await navigator.bluetooth.requestDevice({
filters: [{name: 'Test Device', services: ['heart_rate']}]});
let gatt = await device.gatt.connect();
let service = await gatt.getPrimaryService('heart_rate');
return service.uuid;
} catch(e) {
return `${e.name}: ${e.message}`;
}
})())"));
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTestWithNewPermissionsBackendEnabled,
WebBluetoothPersistentServices) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
AddFakeDevice(kDeviceAddress);
SetDeviceToSelect(kDeviceAddress);
EXPECT_EQ(kHeartRateUUIDString,
content::EvalJs(web_contents_.get(), R"((async() => {
try {
let device = await navigator.bluetooth.requestDevice({
filters: [{name: 'Test Device'}]});
let gatt = await device.gatt.connect();
let service = await gatt.getPrimaryService('heart_rate');
return service.uuid;
} catch(e) {
return `${e.name}: ${e.message}`;
}
})())"));
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTestWithNewPermissionsBackendEnabled,
RevokingPermissionDisconnectsTheDevice) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
AddFakeDevice(kDeviceAddress);
SetDeviceToSelect(kDeviceAddress);
EXPECT_EQ(kHeartRateUUIDString, content::EvalJs(web_contents_.get(), R"(
var gatt;
var gattserverdisconnectedPromise;
(async() => {
try {
let device = await navigator.bluetooth.requestDevice({
filters: [{name: 'Test Device', services: ['heart_rate']}]});
gatt = await device.gatt.connect();
gattserverdisconnectedPromise = new Promise(resolve => {
device.addEventListener('gattserverdisconnected', _ => {
resolve("event fired");
});
});
let service = await gatt.getPrimaryService('heart_rate');
return service.uuid;
} catch(e) {
return `${e.name}: ${e.message}`;
}
})()
)"));
permissions::BluetoothChooserContext* context =
BluetoothChooserContextFactory::GetForProfile(browser()->profile());
url::Origin origin =
web_contents_->GetPrimaryMainFrame()->GetLastCommittedOrigin();
const auto objects = context->GetGrantedObjects(origin);
EXPECT_EQ(1ul, objects.size());
context->RevokeObjectPermission(origin, objects.at(0)->value);
EXPECT_EQ("event fired", content::EvalJs(web_contents_.get(),
"gattserverdisconnectedPromise "));
EXPECT_THAT(content::EvalJs(web_contents_.get(), R"((async() => {
try {
let service = await gatt.getPrimaryService('heart_rate');
return service.uuid;
} catch(e) {
return `${e.name}: ${e.message}`;
}
})())")
.ExtractString(),
::testing::HasSubstr("GATT Server is disconnected."));
}
IN_PROC_BROWSER_TEST_F(WebBluetoothTestWithNewPermissionsBackendEnabled,
RevokingPermissionStopsAdvertisements) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL(kExampleUrl)));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
CheckLastCommitedOrigin(kExampleUrl);
AddFakeDevice(kDeviceAddress);
SetDeviceToSelect(kDeviceAddress);
EXPECT_EQ("", content::EvalJs(web_contents_.get(), R"(
var events_seen = "";
var first_device_promise;
(async() => {
try {
let device = await navigator.bluetooth.requestDevice({
filters: [{name: 'Test Device', services: ['heart_rate']}]});
device.watchAdvertisements();
first_device_promise = new Promise(resolve => {
device.addEventListener('advertisementreceived', event => {
events_seen += event.name + "|";
resolve(events_seen);
});
});
return "";
} catch(e) {
return `${e.name}: ${e.message}`;
}
})()
)"));
url::Origin origin =
web_contents_->GetPrimaryMainFrame()->GetLastCommittedOrigin();
permissions::BluetoothChooserContext* context =
BluetoothChooserContextFactory::GetForProfile(browser()->profile());
auto objects = context->GetGrantedObjects(origin);
ASSERT_EQ(1u, objects.size());
const auto first_object_key = context->GetKeyForObject(objects.at(0)->value);
AddFakeDevice(kDeviceAddress2);
SetDeviceToSelect(kDeviceAddress2);
EXPECT_EQ("", content::EvalJs(web_contents_.get(), R"(
var second_device_promise;
(async() => {
try {
let device = await navigator.bluetooth.requestDevice({
filters: [{name: 'Test Device', services: ['heart_rate']}]});
device.watchAdvertisements();
second_device_promise = new Promise(resolve => {
device.addEventListener('advertisementreceived', event => {
events_seen += 'second_device_' + event.name;
resolve(events_seen);
});
});
return "";
} catch(e) {
return `${e.name}: ${e.message}`;
}
})()
)"));
objects = context->GetGrantedObjects(origin);
EXPECT_EQ(2u, objects.size());
adapter_->SimulateDeviceAdvertisementReceived(kDeviceAddress,
"advertisement_name1");
EXPECT_EQ("advertisement_name1|",
content::EvalJs(web_contents_.get(), "first_device_promise"));
context->RevokeObjectPermission(origin, first_object_key);
EXPECT_EQ(1ul, context->GetGrantedObjects(origin).size());
adapter_->SimulateDeviceAdvertisementReceived(kDeviceAddress,
"advertisement_name2");
adapter_->SimulateDeviceAdvertisementReceived(kDeviceAddress2,
"advertisement_name2");
EXPECT_EQ("advertisement_name1|second_device_advertisement_name2",
content::EvalJs(web_contents_.get(), "second_device_promise"));
}
class WebBluetoothTestWithNewPermissionsBackendEnabledInPrerendering
: public WebBluetoothTestWithNewPermissionsBackendEnabled {
public:
WebBluetoothTestWithNewPermissionsBackendEnabledInPrerendering()
: prerender_helper_(base::BindRepeating(
&WebBluetoothTestWithNewPermissionsBackendEnabledInPrerendering::
GetWebContents,
base::Unretained(this))) {}
~WebBluetoothTestWithNewPermissionsBackendEnabledInPrerendering() override =
default;
void SetUp() override {
prerender_helper_.RegisterServerRequestMonitor(embedded_test_server());
WebBluetoothTestWithNewPermissionsBackendEnabled::SetUp();
}
void SetUpOnMainThread() override {
WebBluetoothTestWithNewPermissionsBackendEnabled::SetUpOnMainThread();
ASSERT_TRUE(test_server_handle_ =
embedded_test_server()->StartAndReturnHandle());
auto url = embedded_test_server()->GetURL("/empty.html");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
web_contents_ = browser()->tab_strip_model()->GetActiveWebContents();
}
content::WebContents* GetWebContents() { return web_contents_; }
content::test::PrerenderTestHelper* prerender_helper() {
return &prerender_helper_;
}
private:
content::test::PrerenderTestHelper prerender_helper_;
net::test_server::EmbeddedTestServerHandle test_server_handle_;
};
class TestWebContentsObserver : public content::WebContentsObserver {
public:
explicit TestWebContentsObserver(content::WebContents* contents)
: WebContentsObserver(contents) {}
TestWebContentsObserver(const TestWebContentsObserver&) = delete;
TestWebContentsObserver& operator=(const TestWebContentsObserver&) = delete;
~TestWebContentsObserver() override = default;
void OnCapabilityTypesChanged(
content::WebContentsCapabilityType capability_type,
bool used) override {
EXPECT_EQ(capability_type,
content::WebContentsCapabilityType::kBluetoothConnected);
++num_capability_types_changed_;
last_device_used_ = used;
if (quit_closure_ &&
expected_updating_count_ == num_capability_types_changed_) {
std::move(quit_closure_).Run();
}
}
int num_capability_types_changed() { return num_capability_types_changed_; }
const std::optional<bool>& last_device_used() { return last_device_used_; }
void clear_last_device_used() { last_device_used_.reset(); }
void WaitUntilConnectionIsUpdated(int expected_count) {
if (num_capability_types_changed_ == expected_count) {
return;
}
expected_updating_count_ = expected_count;
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
run_loop.Run();
}
private:
int num_capability_types_changed_ = 0;
std::optional<bool> last_device_used_;
int expected_updating_count_;
base::OnceClosure quit_closure_;
};
IN_PROC_BROWSER_TEST_F(
WebBluetoothTestWithNewPermissionsBackendEnabledInPrerendering,
WebBluetoothDeviceConnectInPrerendering) {
TestWebContentsObserver observer(GetWebContents());
AddFakeDevice(kDeviceAddress);
SetDeviceToSelect(kDeviceAddress);
ASSERT_TRUE(content::ExecJs(GetWebContents(), R"((async() => {
try {
let device = await navigator.bluetooth.requestDevice({
filters: [{name: 'Test Device'}]});
let gatt = await device.gatt.connect();
let service = await gatt.getPrimaryService('heart_rate');
return service.uuid;
} catch(e) {
return `${e.name}: ${e.message}`;
}
})())"));
observer.WaitUntilConnectionIsUpdated(1);
EXPECT_EQ(observer.num_capability_types_changed(), 1);
EXPECT_TRUE(observer.last_device_used().has_value());
EXPECT_TRUE(observer.last_device_used().value());
observer.clear_last_device_used();
auto prerender_url = embedded_test_server()->GetURL("/simple.html");
content::FrameTreeNodeId host_id =
prerender_helper()->AddPrerender(prerender_url);
content::test::PrerenderHostObserver host_observer(*GetWebContents(),
host_id);
content::RenderFrameHost* prerendered_frame_host =
prerender_helper()->GetPrerenderedMainFrameHost(host_id);
constexpr char kUserGestureError[] =
"Must be handling a user gesture to show a permission request.";
auto result =
content::EvalJs(prerendered_frame_host, R"(
navigator.bluetooth.requestDevice({
filters: [{name: 'Test Device', services: ['heart_rate']}]}))",
content::EvalJsOptions::EXECUTE_SCRIPT_NO_USER_GESTURE);
EXPECT_THAT(result, content::EvalJsResult::ErrorIs(
::testing::HasSubstr(kUserGestureError)));
EXPECT_EQ(observer.num_capability_types_changed(), 1);
EXPECT_FALSE(observer.last_device_used().has_value());
content::RenderFrameDeletedObserver rfh_observer(
GetWebContents()->GetPrimaryMainFrame());
prerender_helper()->NavigatePrimaryPage(prerender_url);
EXPECT_TRUE(host_observer.was_activated());
rfh_observer.WaitUntilDeleted();
EXPECT_EQ(observer.num_capability_types_changed(), 2);
EXPECT_TRUE(observer.last_device_used().has_value());
EXPECT_FALSE(observer.last_device_used().value());
}
}