chromium/ash/quick_pair/scanning/fast_pair/fast_pair_scanner_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 "ash/quick_pair/scanning/fast_pair/fast_pair_scanner_impl.h"

#include <memory>

#include "ash/constants/ash_features.h"
#include "ash/quick_pair/common/constants.h"
#include "ash/quick_pair/common/device.h"
#include "ash/quick_pair/common/fake_bluetooth_adapter.h"
#include "ash/quick_pair/common/protocol.h"
#include "ash/quick_pair/fast_pair_handshake/fake_fast_pair_handshake.h"
#include "ash/quick_pair/fast_pair_handshake/fake_fast_pair_handshake_lookup.h"
#include "ash/quick_pair/fast_pair_handshake/fast_pair_data_encryptor.h"
#include "ash/quick_pair/fast_pair_handshake/fast_pair_gatt_service_client.h"
#include "ash/quick_pair/fast_pair_handshake/fast_pair_handshake.h"
#include "ash/quick_pair/fast_pair_handshake/fast_pair_handshake_lookup.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/ranges/algorithm.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/bluetooth_low_energy_scan_filter.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/bluetooth/test/mock_bluetooth_device.h"
#include "device/bluetooth/test/mock_bluetooth_low_energy_scan_session.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

// Below constants are used to construct MockBluetoothDevice for testing.
constexpr char kTestBleDeviceAddress1[] = "11:12:13:14:15:16";
constexpr char kTestBleDeviceName[] = "Test Device Name";

std::unique_ptr<device::MockBluetoothDevice> CreateTestBluetoothDevice(
    const std::string& address) {
  auto mock_device =
      std::make_unique<testing::NiceMock<device::MockBluetoothDevice>>(
          /*adapter=*/nullptr, /*bluetooth_class=*/0, kTestBleDeviceName,
          address, /*paired=*/true, /*connected=*/true);
  mock_device->AddUUID(ash::quick_pair::kFastPairBluetoothUuid);
  mock_device->SetServiceDataForUUID(ash::quick_pair::kFastPairBluetoothUuid,
                                     {1, 2, 3});
  return mock_device;
}

class FastPairScannerObserver
    : public ash::quick_pair::FastPairScanner::Observer {
 public:
  // FastPairScanner::Observer overrides
  void OnDeviceFound(device::BluetoothDevice* device) override {
    device_addreses_.push_back(device->GetAddress());
    on_device_found_count_++;
  }

  void OnDeviceLost(device::BluetoothDevice* device) override {
    device_addreses_.erase(
        base::ranges::find(device_addreses_, device->GetAddress()));
  }

  bool DoesDeviceListContainTestDevice(const std::string& address) {
    return base::Contains(device_addreses_, address);
  }

  int on_device_found_count() { return on_device_found_count_; }

 private:
  std::vector<std::string> device_addreses_;
  int on_device_found_count_ = 0;
};

}  // namespace

namespace ash {
namespace quick_pair {

class FastPairScannerImplTest : public testing::Test {
 public:
  void SetUp() override {
    adapter_ = base::MakeRefCounted<FakeBluetoothAdapter>();
    ON_CALL(*adapter_, StartLowEnergyScanSession(testing::_, testing::_))
        .WillByDefault(
            Invoke(this, &FastPairScannerImplTest::StartLowEnergyScanSession));
    device::BluetoothAdapterFactory::SetAdapterForTesting(adapter_);
    FastPairHandshakeLookup::UseFakeInstance();

    scanner_ = base::MakeRefCounted<FastPairScannerImpl>();
    scanner_observer_ = std::make_unique<FastPairScannerObserver>();
    scanner().AddObserver(scanner_observer_.get());

    task_environment_.RunUntilIdle();
  }

  void TearDown() override {
    scanner().RemoveObserver(scanner_observer_.get());
    FastPairHandshakeLookup::GetInstance()->Clear();
  }

  std::unique_ptr<device::BluetoothLowEnergyScanSession>
  StartLowEnergyScanSession(
      std::unique_ptr<device::BluetoothLowEnergyScanFilter> filter,
      base::WeakPtr<device::BluetoothLowEnergyScanSession::Delegate> delegate) {
    auto mock_scan_session =
        std::make_unique<device::MockBluetoothLowEnergyScanSession>(
            base::BindOnce(&FastPairScannerImplTest::OnScanSessionDestroyed,
                           weak_ptr_factory_.GetWeakPtr()));
    mock_scan_session_ = mock_scan_session.get();
    delegate_ = delegate;
    return mock_scan_session;
  }

  void OnScanSessionDestroyed() {
    delegate_ = nullptr;
    mock_scan_session_ = nullptr;
  }

  FakeBluetoothAdapter& adapter() { return *(adapter_.get()); }

  device::MockBluetoothLowEnergyScanSession* scan_session() {
    return mock_scan_session_;
  }

  FastPairScannerObserver& scanner_observer() {
    return *(scanner_observer_.get());
  }

  FastPairScanner& scanner() { return *(scanner_.get()); }

  void TriggerOnDeviceFound(const std::string& address) {
    if (!delegate_)
      return;

    delegate_->OnDeviceFound(mock_scan_session_,
                             CreateTestBluetoothDevice(address).get());
  }

  void TriggerOnDeviceLost(const std::string& address) {
    if (!delegate_)
      return;

    delegate_->OnDeviceLost(mock_scan_session_,
                            CreateTestBluetoothDevice(address).get());
  }

  void AddConnectedHandshake(const std::string& address) {
    FastPairHandshakeLookup::GetInstance()->Create(
        adapter_,
        base::MakeRefCounted<Device>("", address, Protocol::kFastPairInitial),
        base::DoNothing());
  }

  std::unique_ptr<FastPairHandshake> CreateConnectedHandshake(
      scoped_refptr<Device> device,
      FastPairHandshakeLookup::OnCompleteCallback callback) {
    return std::make_unique<FakeFastPairHandshake>(adapter_, std::move(device),
                                                   std::move(callback));
  }

  void SetUpFactoryScanner() {
    scanner_.reset();
    scanner_ = FastPairScannerImpl::Factory::Create();
    scanner_->AddObserver(scanner_observer_.get());
    task_environment_.RunUntilIdle();
  }

 protected:
  base::test::SingleThreadTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  scoped_refptr<FakeBluetoothAdapter> adapter_;
  scoped_refptr<FastPairScanner> scanner_;
  raw_ptr<device::MockBluetoothLowEnergyScanSession> mock_scan_session_ =
      nullptr;
  std::unique_ptr<FastPairScannerObserver> scanner_observer_;
  base::WeakPtr<device::BluetoothLowEnergyScanSession::Delegate> delegate_;
  base::WeakPtrFactory<FastPairScannerImplTest> weak_ptr_factory_{this};
};

TEST_F(FastPairScannerImplTest, FactoryCreate) {
  SetUpFactoryScanner();
  TriggerOnDeviceFound(kTestBleDeviceAddress1);
  EXPECT_TRUE(scanner_observer().DoesDeviceListContainTestDevice(
      kTestBleDeviceAddress1));
}

TEST_F(FastPairScannerImplTest, SessionStartedSuccessfully) {
  delegate_->OnSessionStarted(mock_scan_session_,
                              /*error_code=*/std::nullopt);
  TriggerOnDeviceFound(kTestBleDeviceAddress1);
  EXPECT_TRUE(scanner_observer().DoesDeviceListContainTestDevice(
      kTestBleDeviceAddress1));
}

TEST_F(FastPairScannerImplTest, SessionStartedFailure) {
  delegate_->OnSessionStarted(
      mock_scan_session_,
      device::BluetoothLowEnergyScanSession::ErrorCode::kFailed);
  delegate_ = nullptr;
  TriggerOnDeviceFound(kTestBleDeviceAddress1);
  EXPECT_FALSE(scanner_observer().DoesDeviceListContainTestDevice(
      kTestBleDeviceAddress1));
}

TEST_F(FastPairScannerImplTest, SessionInvalidated) {
  delegate_->OnSessionInvalidated(mock_scan_session_);
  TriggerOnDeviceFound(kTestBleDeviceAddress1);
  EXPECT_FALSE(scanner_observer().DoesDeviceListContainTestDevice(
      kTestBleDeviceAddress1));
}

TEST_F(FastPairScannerImplTest, DeviceAddedNotifiesObservers) {
  TriggerOnDeviceFound(kTestBleDeviceAddress1);
  EXPECT_TRUE(scanner_observer().DoesDeviceListContainTestDevice(
      kTestBleDeviceAddress1));
}

TEST_F(FastPairScannerImplTest, DeviceRemoved) {
  TriggerOnDeviceFound(kTestBleDeviceAddress1);
  EXPECT_EQ(scanner_observer().on_device_found_count(), 1);

  auto mock_device = CreateTestBluetoothDevice(kTestBleDeviceAddress1);
  auto* mock_device_ptr = mock_device.get();
  mock_device->SetServiceDataForUUID(ash::quick_pair::kFastPairBluetoothUuid,
                                     {4, 5, 6});

  adapter().NotifyDeviceRemoved(mock_device_ptr);
  adapter().NotifyDeviceChanged(mock_device_ptr);
  EXPECT_EQ(scanner_observer().on_device_found_count(), 1);
}

TEST_F(FastPairScannerImplTest, DevicePairedChanged) {
  TriggerOnDeviceFound(kTestBleDeviceAddress1);
  EXPECT_EQ(scanner_observer().on_device_found_count(), 1);

  auto mock_device = CreateTestBluetoothDevice(kTestBleDeviceAddress1);
  auto* mock_device_ptr = mock_device.get();
  mock_device->SetServiceDataForUUID(ash::quick_pair::kFastPairBluetoothUuid,
                                     {4, 5, 6});

  adapter().NotifyDevicePairedChanged(mock_device_ptr, false);
  adapter().NotifyDeviceChanged(mock_device_ptr);
  EXPECT_EQ(scanner_observer().on_device_found_count(), 1);
}

TEST_F(FastPairScannerImplTest, DeviceAddedAlreadyHasHandshake) {
  AddConnectedHandshake(kTestBleDeviceAddress1);
  TriggerOnDeviceFound(kTestBleDeviceAddress1);
  EXPECT_FALSE(scanner_observer().DoesDeviceListContainTestDevice(
      kTestBleDeviceAddress1));
}

TEST_F(FastPairScannerImplTest, DeviceLostNotifiesObservers) {
  TriggerOnDeviceFound(kTestBleDeviceAddress1);
  EXPECT_TRUE(scanner_observer().DoesDeviceListContainTestDevice(
      kTestBleDeviceAddress1));
  TriggerOnDeviceLost(kTestBleDeviceAddress1);
  EXPECT_FALSE(scanner_observer().DoesDeviceListContainTestDevice(
      kTestBleDeviceAddress1));
}

TEST_F(FastPairScannerImplTest, DeviceChangedNewServiceDataLength) {
  TriggerOnDeviceFound(kTestBleDeviceAddress1);
  EXPECT_EQ(scanner_observer().on_device_found_count(), 1);

  auto mock_device = CreateTestBluetoothDevice(kTestBleDeviceAddress1);
  auto* mock_device_ptr = mock_device.get();

  // The length of the service data changes between Initial/Subsequent pairing
  // which is used to detect if we should trigger OnDeviceFound or not.
  mock_device->SetServiceDataForUUID(ash::quick_pair::kFastPairBluetoothUuid,
                                     {4, 5, 6, 7});
  adapter().NotifyDeviceChanged(mock_device_ptr);
  EXPECT_EQ(scanner_observer().on_device_found_count(), 2);
}

TEST_F(FastPairScannerImplTest, DeviceChangedSameServiceDataLength) {
  TriggerOnDeviceFound(kTestBleDeviceAddress1);
  EXPECT_EQ(scanner_observer().on_device_found_count(), 1);

  auto mock_device = CreateTestBluetoothDevice(kTestBleDeviceAddress1);
  auto* mock_device_ptr = mock_device.get();
  mock_device->SetServiceDataForUUID(ash::quick_pair::kFastPairBluetoothUuid,
                                     {4, 5, 6});
  // This simulates a change of service data within one of the ongoing pairing
  // scenarios, in which case we do not notify observers.
  adapter().NotifyDeviceChanged(mock_device_ptr);
  EXPECT_EQ(scanner_observer().on_device_found_count(), 1);
}

TEST_F(FastPairScannerImplTest, DeviceAddedNoServiceData) {
  auto mock_device =
      std::make_unique<testing::NiceMock<device::MockBluetoothDevice>>(
          /*adapter=*/nullptr, /*bluetooth_class=*/0, kTestBleDeviceName,
          kTestBleDeviceAddress1, /*paired=*/true, /*connected=*/true);
  delegate_->OnDeviceFound(mock_scan_session_, mock_device.get());
  EXPECT_EQ(scanner_observer().on_device_found_count(), 0);
}

TEST_F(FastPairScannerImplTest, DeviceChangedNoServiceData) {
  auto mock_device =
      std::make_unique<testing::NiceMock<device::MockBluetoothDevice>>(
          /*adapter=*/nullptr, /*bluetooth_class=*/0, kTestBleDeviceName,
          kTestBleDeviceAddress1, /*paired=*/true, /*connected=*/true);
  delegate_->OnDeviceFound(mock_scan_session_, mock_device.get());
  EXPECT_EQ(scanner_observer().on_device_found_count(), 0);
  auto* mock_device_ptr = mock_device.get();
  mock_device->SetServiceDataForUUID(ash::quick_pair::kFastPairBluetoothUuid,
                                     {4, 5, 6});
  adapter().NotifyDeviceChanged(mock_device_ptr);
  EXPECT_EQ(scanner_observer().on_device_found_count(), 0);
}

TEST_F(FastPairScannerImplTest, IgnoresEventDuringActiveHandshake) {
  TriggerOnDeviceFound(kTestBleDeviceAddress1);
  EXPECT_TRUE(scanner_observer().DoesDeviceListContainTestDevice(
      kTestBleDeviceAddress1));
  AddConnectedHandshake(kTestBleDeviceAddress1);
  TriggerOnDeviceLost(kTestBleDeviceAddress1);
  EXPECT_FALSE(scanner_observer().DoesDeviceListContainTestDevice(
      kTestBleDeviceAddress1));
  TriggerOnDeviceFound(kTestBleDeviceAddress1);
  EXPECT_TRUE(scanner_observer().DoesDeviceListContainTestDevice(
      kTestBleDeviceAddress1));
  EXPECT_EQ(scanner_observer().on_device_found_count(), 2);
}

TEST_F(FastPairScannerImplTest, NoNotifyForPairedDevice) {
  auto paired_device = base::MakeRefCounted<Device>(
      "test_metadata_id", kTestBleDeviceAddress1, Protocol::kFastPairInitial);
  paired_device->set_classic_address(kTestBleDeviceAddress1);

  auto mock_device =
      std::make_unique<testing::NiceMock<device::MockBluetoothDevice>>(
          /*adapter=*/nullptr, /*bluetooth_class=*/0, kTestBleDeviceName,
          kTestBleDeviceAddress1, /*paired=*/true, /*connected=*/true);

  adapter_->AddMockDevice(std::move(mock_device));

  scanner_->OnDevicePaired(paired_device);
  TriggerOnDeviceFound(kTestBleDeviceAddress1);

  EXPECT_EQ(scanner_observer().on_device_found_count(), 0);
}

}  // namespace quick_pair
}  // namespace ash