chromium/chromeos/ash/services/bluetooth_config/device_operation_handler_impl_unittest.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/services/bluetooth_config/device_operation_handler_impl.h"

#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/time/clock.h"
#include "chromeos/ash/services/bluetooth_config/fake_adapter_state_controller.h"
#include "chromeos/ash/services/bluetooth_config/fake_device_name_manager.h"
#include "chromeos/ash/services/bluetooth_config/fake_fast_pair_delegate.h"
#include "device/bluetooth/chromeos/bluetooth_utils.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/bluetooth/test/mock_bluetooth_device.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash::bluetooth_config {

namespace {

using NiceMockDevice =
    std::unique_ptr<testing::NiceMock<device::MockBluetoothDevice>>;

const uint32_t kTestBluetoothClass = 1337u;
const char kTestBluetoothName[] = "testName";
const char kTestBluetoothNickname[] = "testNickname";
const base::TimeDelta kTestDuration = base::Milliseconds(1000);

}  // namespace

class DeviceOperationHandlerImplTest : public testing::Test {
 protected:
  using Operation = DeviceOperationHandler::Operation;

  DeviceOperationHandlerImplTest()
      : task_environment_(
            base::test::SingleThreadTaskEnvironment::MainThreadType::UI,
            base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  DeviceOperationHandlerImplTest(const DeviceOperationHandlerImplTest&) =
      delete;
  DeviceOperationHandlerImplTest& operator=(
      const DeviceOperationHandlerImplTest&) = delete;
  ~DeviceOperationHandlerImplTest() override = default;

  // testing::Test:
  void SetUp() override {
    mock_adapter_ =
        base::MakeRefCounted<testing::NiceMock<device::MockBluetoothAdapter>>();
    ON_CALL(*mock_adapter_, GetDevices())
        .WillByDefault(testing::Invoke(
            this, &DeviceOperationHandlerImplTest::GetMockDevices));

    device_operation_handler_ = std::make_unique<DeviceOperationHandlerImpl>(
        &fake_adapter_state_controller_, mock_adapter_,
        &fake_device_name_manager_, &fake_fast_pair_delegate_);
  }

  void SetBluetoothSystemState(mojom::BluetoothSystemState system_state) {
    fake_adapter_state_controller_.SetSystemState(system_state);
  }

  void AssertHistogramMetrics(int success_count, int failure_count) {
    histogram_tester.ExpectBucketCount(
        "Bluetooth.ChromeOS.UserInitiatedReconnectionAttempt.Result",
        /*success=*/false, failure_count);
    histogram_tester.ExpectBucketCount(
        "Bluetooth.ChromeOS.UserInitiatedReconnectionAttempt.Result",
        /*success=*/true, success_count);
  }

  void AssertDurationHistogramMetrics(base::TimeDelta bucket,
                                      int success_count,
                                      int failure_count,
                                      std::string transport_name) {
    histogram_tester.ExpectBucketCount(
        "Bluetooth.ChromeOS.UserInitiatedReconnectionAttempt.Duration.Success",
        bucket.InMilliseconds(), success_count);
    histogram_tester.ExpectBucketCount(
        base::StrCat({"Bluetooth.ChromeOS.UserInitiatedReconnectionAttempt."
                      "Duration.Success.",
                      transport_name}),
        bucket.InMilliseconds(), success_count);
    histogram_tester.ExpectBucketCount(
        "Bluetooth.ChromeOS.UserInitiatedReconnectionAttempt.Duration.Failure",
        bucket.InMilliseconds(), failure_count);
    histogram_tester.ExpectBucketCount(
        base::StrCat({"Bluetooth.ChromeOS.UserInitiatedReconnectionAttempt."
                      "Duration.Failure.",
                      transport_name}),
        bucket.InMilliseconds(), failure_count);
  }

  void AddDevice(std::string* id_out) {
    // We use the number of devices created in this test as the address.
    std::string address = base::NumberToString(num_devices_created_);
    ++num_devices_created_;

    // Mock devices have their ID set to "${address}-Identifier".
    *id_out = base::StrCat({address, "-Identifier"});

    auto mock_device =
        std::make_unique<testing::NiceMock<device::MockBluetoothDevice>>(
            mock_adapter_.get(), kTestBluetoothClass, kTestBluetoothName,
            address, /*paired=*/false, /*connected=*/false);

    ON_CALL(*mock_device, ConnectClassic(testing::_, testing::_))
        .WillByDefault(testing::Invoke(
            [this](device::BluetoothDevice::PairingDelegate* pairing_delegate,
                   device::BluetoothDevice::ConnectCallback callback) {
              EXPECT_FALSE(connect_callback_);
              connect_callback_ = std::move(callback);
            }));
    ON_CALL(*mock_device, Disconnect(testing::_, testing::_))
        .WillByDefault(testing::Invoke(
            [this](base::OnceClosure callback,
                   device::BluetoothDevice::ErrorCallback error_callback) {
              EXPECT_FALSE(disconnect_callbacks_.has_value());
              disconnect_callbacks_ = std::make_pair(std::move(callback),
                                                     std::move(error_callback));
            }));
    ON_CALL(*mock_device, Forget(testing::_, testing::_))
        .WillByDefault(testing::Invoke(
            [this](base::OnceClosure callback,
                   device::BluetoothDevice::ErrorCallback error_callback) {
              EXPECT_FALSE(forget_callbacks_.has_value());
              forget_callbacks_ = std::make_pair(std::move(callback),
                                                 std::move(error_callback));
            }));
    ON_CALL(*mock_device, GetType()).WillByDefault(testing::Invoke([]() {
      return device::BluetoothTransport::BLUETOOTH_TRANSPORT_CLASSIC;
    }));

    mock_devices_.push_back(std::move(mock_device));
  }

  void ConnectDevice(const std::string& device_id) {
    device_operation_handler_->Connect(
        device_id,
        base::BindOnce(&DeviceOperationHandlerImplTest::OnOperationFinished,
                       base::Unretained(this), device_id, Operation::kConnect));
  }

  void DisconnectDevice(const std::string& device_id) {
    device_operation_handler_->Disconnect(
        device_id,
        base::BindOnce(&DeviceOperationHandlerImplTest::OnOperationFinished,
                       base::Unretained(this), device_id,
                       Operation::kDisconnect));
  }

  void ForgetDevice(const std::string& device_id) {
    device_operation_handler_->Forget(
        device_id,
        base::BindOnce(&DeviceOperationHandlerImplTest::OnOperationFinished,
                       base::Unretained(this), device_id, Operation::kForget));
  }

  void OnOperationFinished(const std::string& device_id,
                           Operation operation,
                           bool success) {
    results_.emplace_back(device_id, operation, success);
  }

  base::TimeDelta GetOperationTimeout() {
    return DeviceOperationHandler::kOperationTimeout;
  }

  void FastForwardOperation(base::TimeDelta time) {
    task_environment_.FastForwardBy(time);
  }

  const std::vector<std::tuple<std::string, Operation, bool>>& results() {
    return results_;
  }

  bool HasPendingConnectCallback() const {
    return !connect_callback_.is_null();
  }

  void InvokePendingConnectCallback(bool success) {
    if (success) {
      std::move(connect_callback_).Run(std::nullopt);
    } else {
      std::move(connect_callback_)
          .Run(device::BluetoothDevice::ConnectErrorCode::ERROR_FAILED);
    }
  }

  bool HasPendingDisconnectCallback() const {
    return disconnect_callbacks_.has_value();
  }

  void InvokePendingDisconnectCallback(bool success) {
    if (success) {
      std::move(disconnect_callbacks_->first).Run();
    } else {
      std::move(disconnect_callbacks_->second).Run();
    }
    disconnect_callbacks_.reset();
  }

  bool HasPendingForgetCallback() const {
    return forget_callbacks_.has_value();
  }

  void InvokePendingForgetCallback(bool success) {
    if (success) {
      std::move(forget_callbacks_->first).Run();
    } else {
      std::move(forget_callbacks_->second).Run();
    }
    forget_callbacks_.reset();
  }

  void SetDeviceNickname(const std::string& device_id) {
    fake_device_name_manager_.SetDeviceNickname(device_id,
                                                kTestBluetoothNickname);
  }

  std::vector<std::string> GetFastPairDeletedDevices() {
    return fake_fast_pair_delegate_.forgotten_device_addresses();
  }

  std::optional<std::string> GetDeviceNickname(const std::string& device_id) {
    return fake_device_name_manager_.GetDeviceNickname(device_id);
  }

  base::HistogramTester histogram_tester;

 private:
  std::vector<raw_ptr<const device::BluetoothDevice, VectorExperimental>>
  GetMockDevices() {
    std::vector<raw_ptr<const device::BluetoothDevice, VectorExperimental>>
        devices;
    for (auto& device : mock_devices_)
      devices.push_back(device.get());
    return devices;
  }

  base::test::TaskEnvironment task_environment_;

  // Results of processed operations. Each entry contains the operation's
  // device ID, which Operation it was, and if it succeeded or not.
  std::vector<std::tuple<std::string, Operation, bool>> results_;

  device::BluetoothDevice::ConnectCallback connect_callback_;
  std::optional<
      std::pair<base::OnceClosure, device::BluetoothDevice::ErrorCallback>>
      disconnect_callbacks_;
  std::optional<
      std::pair<base::OnceClosure, device::BluetoothDevice::ErrorCallback>>
      forget_callbacks_;

  std::vector<NiceMockDevice> mock_devices_;
  size_t num_devices_created_ = 0u;

  FakeAdapterStateController fake_adapter_state_controller_;
  scoped_refptr<testing::NiceMock<device::MockBluetoothAdapter>> mock_adapter_;
  FakeDeviceNameManager fake_device_name_manager_;
  FakeFastPairDelegate fake_fast_pair_delegate_;

  std::unique_ptr<DeviceOperationHandlerImpl> device_operation_handler_;
};

TEST_F(DeviceOperationHandlerImplTest,
       ConnectBluetoothDisabledThenNotFoundThenFailThenSucceed) {
  std::string device_id = "testid";

  // Connect should fail due to Bluetooth being disabled.
  SetBluetoothSystemState(mojom::BluetoothSystemState::kDisabled);
  ConnectDevice(device_id);
  EXPECT_EQ(results()[0], std::make_tuple(device_id, Operation::kConnect,
                                          /*success=*/false));
  AssertHistogramMetrics(/*success_count=*/0, /*failure_count=*/1);
  AssertDurationHistogramMetrics(base::Milliseconds(0), /*success_count=*/0,
                                 /*failure_count=*/1,
                                 /*transport_name=*/"Invalid");

  // Connect should fail due to device not being found.
  SetBluetoothSystemState(mojom::BluetoothSystemState::kEnabled);
  ConnectDevice(device_id);
  EXPECT_EQ(results()[1],
            std::make_tuple(device_id, Operation::kConnect, false));
  AssertHistogramMetrics(/*success_count=*/0, /*failure_count=*/2);
  AssertDurationHistogramMetrics(base::Milliseconds(0), /*success_count=*/0,
                                 /*failure_count=*/2,
                                 /*transport_name=*/"Invalid");

  // Add the device and simulate BluetoothDevice::Connect() failing.
  AddDevice(&device_id);
  ConnectDevice(device_id);
  FastForwardOperation(kTestDuration);
  EXPECT_TRUE(HasPendingConnectCallback());
  InvokePendingConnectCallback(/*success=*/false);
  EXPECT_EQ(results()[2], std::make_tuple(device_id, Operation::kConnect,
                                          /*success=*/false));

  AssertHistogramMetrics(/*success_count=*/0, /*failure_count=*/3);
  AssertDurationHistogramMetrics(kTestDuration, /*success_count=*/0,
                                 /*failure_count=*/1,
                                 /*transport_name=*/"Classic");

  // Simulate BluetoothDevice::Connect() succeeding.
  ConnectDevice(device_id);
  EXPECT_TRUE(HasPendingConnectCallback());
  FastForwardOperation(kTestDuration);
  InvokePendingConnectCallback(/*success=*/true);
  EXPECT_EQ(results()[3], std::make_tuple(device_id, Operation::kConnect,
                                          /*success=*/true));
  AssertHistogramMetrics(/*success_count=*/1, /*failure_count=*/3);
  AssertDurationHistogramMetrics(kTestDuration, /*success_count=*/1,
                                 /*failure_count=*/1,
                                 /*transport_name=*/"Classic");
}

TEST_F(DeviceOperationHandlerImplTest, DisconnectNotFoundFailThenSucceed) {
  std::string device_id = "testid";

  // Disconnect should fail due to device not being found.
  DisconnectDevice(device_id);
  EXPECT_EQ(results()[0], std::make_tuple(device_id, Operation::kDisconnect,
                                          /*success=*/false));

  // Add the device and simulate BluetoothDevice::Disconnect() failing.
  AddDevice(&device_id);
  DisconnectDevice(device_id);
  EXPECT_TRUE(HasPendingDisconnectCallback());
  InvokePendingDisconnectCallback(/*success=*/false);
  EXPECT_EQ(results()[1], std::make_tuple(device_id, Operation::kDisconnect,
                                          /*success=*/false));

  // Simulate BluetoothDevice::Disconnect() succeeding.
  DisconnectDevice(device_id);
  EXPECT_TRUE(HasPendingDisconnectCallback());
  InvokePendingDisconnectCallback(/*success=*/true);
  EXPECT_EQ(results()[2], std::make_tuple(device_id, Operation::kDisconnect,
                                          /*success=*/true));
}

TEST_F(DeviceOperationHandlerImplTest, ForgetNotFoundThenSucceed) {
  std::string device_id = "testid";

  // Forget should fail due to device not being found.
  ForgetDevice(device_id);
  EXPECT_EQ(results()[0], std::make_tuple(device_id, Operation::kForget,
                                          /*success=*/false));

  // Add and forget the device.
  AddDevice(&device_id);
  ForgetDevice(device_id);

  // Forgetting a device will never fail, and the handler will immediately
  // notify that the operation finished successfully, so don't bother checking
  // for pending callbacks.
  EXPECT_EQ(results()[1], std::make_tuple(device_id, Operation::kForget,
                                          /*success=*/true));
}

TEST_F(DeviceOperationHandlerImplTest, ForgettingDeviceCallsFastPairDelegate) {
  std::string device_id;
  AddDevice(&device_id);
  EXPECT_EQ(GetFastPairDeletedDevices().size(), 0u);

  ForgetDevice(device_id);

  EXPECT_EQ(results()[0],
            std::make_tuple(device_id, Operation::kForget, /*success=*/true));
  EXPECT_EQ(GetFastPairDeletedDevices().size(), 1u);

  // Derive the address from the device ID.
  std::string address = device_id.substr(0, device_id.find("-Identifier"));
  EXPECT_EQ(GetFastPairDeletedDevices()[0], address);
}

TEST_F(DeviceOperationHandlerImplTest, ForgettingDeviceRemovesNickname) {
  std::string device_id;
  AddDevice(&device_id);

  SetDeviceNickname(device_id);
  std::optional<std::string> nickname = GetDeviceNickname(device_id);
  EXPECT_TRUE(nickname.has_value());
  EXPECT_EQ(kTestBluetoothNickname, nickname.value());

  ForgetDevice(device_id);
  EXPECT_EQ(results()[0],
            std::make_tuple(device_id, Operation::kForget, /*success=*/true));
  EXPECT_FALSE(GetDeviceNickname(device_id).has_value());
}

TEST_F(DeviceOperationHandlerImplTest, SimultaneousOperationsAreQueued) {
  std::string device_id1 = "device_id1";
  AddDevice(&device_id1);
  std::string device_id2 = "device_id2";
  AddDevice(&device_id2);
  std::string device_id3 = "device_id3";
  AddDevice(&device_id3);

  // Connect to the first device. BluetoothDevice::Connect() should be
  // called.
  ConnectDevice(device_id1);
  EXPECT_TRUE(HasPendingConnectCallback());

  // Attempt to disconnect another device. BluetoothDevice::Disconnect() should
  // not be called yet.
  DisconnectDevice(device_id2);
  EXPECT_FALSE(HasPendingDisconnectCallback());
  FastForwardOperation(kTestDuration);

  // Invoke the first connect callback.
  InvokePendingConnectCallback(/*success=*/false);
  EXPECT_EQ(results()[0], std::make_tuple(device_id1, Operation::kConnect,
                                          /*success=*/false));
  AssertHistogramMetrics(/*success_count=*/0, /*failure_count=*/1);
  AssertDurationHistogramMetrics(kTestDuration, /*success_count=*/0,
                                 /*failure_count=*/1,
                                 /*transport_name=*/"Classic");

  // Now the second operation's BluetoothDevice::Disconnect() should have been
  // called.
  EXPECT_TRUE(HasPendingDisconnectCallback());

  // Attempt to forget a third device and disable Bluetooth.
  // BluetoothDevice::Forget() should not be called yet,
  ForgetDevice(device_id3);
  EXPECT_FALSE(HasPendingForgetCallback());
  SetBluetoothSystemState(mojom::BluetoothSystemState::kDisabled);

  // Succeed with the disconnect call.
  InvokePendingDisconnectCallback(/*success=*/true);
  EXPECT_EQ(results()[1], std::make_tuple(device_id2, Operation::kDisconnect,
                                          /*success=*/true));

  // The forget call should immediately fail due to Bluetooth being disabled.
  EXPECT_EQ(results()[2], std::make_tuple(device_id3, Operation::kForget,
                                          /*success=*/false));
}

TEST_F(DeviceOperationHandlerImplTest, OperationsTimeout) {
  std::string device_id1 = "device_id1";
  AddDevice(&device_id1);
  std::string device_id2 = "device_id2";
  AddDevice(&device_id2);

  // Connect to the first device. BluetoothDevice::Connect() should be
  // called.
  ConnectDevice(device_id1);
  EXPECT_TRUE(HasPendingConnectCallback());

  // Queue disconnecting the second device.
  DisconnectDevice(device_id2);
  EXPECT_FALSE(HasPendingDisconnectCallback());

  // Simulate connect timing out. The operation should finish with a failure
  // result and the disconnect started.
  FastForwardOperation(GetOperationTimeout());
  EXPECT_EQ(results()[0], std::make_tuple(device_id1, Operation::kConnect,
                                          /*success=*/false));
  EXPECT_TRUE(HasPendingDisconnectCallback());
  AssertHistogramMetrics(/*success_count=*/0, /*failure_count=*/1);
  AssertDurationHistogramMetrics(GetOperationTimeout(), /*success_count=*/0,
                                 /*failure_count=*/1,
                                 /*transport_name=*/"Classic");

  // Simulate disconnect timing out. The operation should finish with a failure
  // result.
  FastForwardOperation(GetOperationTimeout());
  EXPECT_EQ(results()[1], std::make_tuple(device_id2, Operation::kDisconnect,
                                          /*success=*/false));
}

TEST_F(DeviceOperationHandlerImplTest, OperationCompletesBeforeTimeout) {
  std::string device_id = "testid";
  AddDevice(&device_id);

  // Connect to device.
  ConnectDevice(device_id);
  FastForwardOperation(kTestDuration);
  EXPECT_TRUE(HasPendingConnectCallback());
  InvokePendingConnectCallback(/*success=*/true);
  EXPECT_EQ(results()[0], std::make_tuple(device_id, Operation::kConnect,
                                          /*success=*/true));
  AssertHistogramMetrics(/*success_count=*/1, /*failure_count=*/0);
  AssertDurationHistogramMetrics(kTestDuration, /*success_count=*/1,
                                 /*failure_count=*/0,
                                 /*transport_name=*/"Classic");

  // Fast forward to where the timer would timeout if it was still running. This
  // will crash if the timer isn't cancelled after the operation finished.
  FastForwardOperation(GetOperationTimeout());
}

TEST_F(DeviceOperationHandlerImplTest, OperationCompletesAfterTimeout) {
  std::string device_id1 = "device_id1";
  AddDevice(&device_id1);
  std::string device_id2 = "device_id2";
  AddDevice(&device_id2);

  // Connect to the first device. BluetoothDevice::Connect() should be
  // called.
  ConnectDevice(device_id1);
  EXPECT_TRUE(HasPendingConnectCallback());

  // Queue disconnecting the second device.
  DisconnectDevice(device_id2);
  EXPECT_FALSE(HasPendingDisconnectCallback());

  // Simulate connect timing out. The operation should finish with a failure
  // result and the disconnect started.
  FastForwardOperation(GetOperationTimeout());
  EXPECT_EQ(results()[0], std::make_tuple(device_id1, Operation::kConnect,
                                          /*success=*/false));
  EXPECT_TRUE(HasPendingDisconnectCallback());
  AssertHistogramMetrics(/*success_count=*/0, /*failure_count=*/1);
  AssertDurationHistogramMetrics(GetOperationTimeout(), /*success_count=*/0,
                                 /*failure_count=*/1,
                                 /*transport_name=*/"Classic");

  // Simulate the connect call now finishing after the timeout. This should not
  // cause the current operation (disconnect) to finish with a result.
  InvokePendingConnectCallback(/*success=*/true);
  EXPECT_EQ(results().size(), 1u);

  // Finish the disconnect operation.
  InvokePendingDisconnectCallback(/*success=*/true);
  EXPECT_EQ(results()[1], std::make_tuple(device_id2, Operation::kDisconnect,
                                          /*success=*/true));
  AssertHistogramMetrics(/*success_count=*/0, /*failure_count=*/1);
  AssertDurationHistogramMetrics(GetOperationTimeout(), /*success_count=*/0,
                                 /*failure_count=*/1,
                                 /*transport_name=*/"Classic");
}

}  // namespace ash::bluetooth_config