chromium/chromeos/ash/services/bluetooth_config/discovery_session_manager_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/discovery_session_manager_impl.h"

#include <memory>
#include <optional>

#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/types/token_type.h"
#include "chromeos/ash/services/bluetooth_config/device_conversion_util.h"
#include "chromeos/ash/services/bluetooth_config/device_pairing_handler_impl.h"
#include "chromeos/ash/services/bluetooth_config/fake_adapter_state_controller.h"
#include "chromeos/ash/services/bluetooth_config/fake_bluetooth_discovery_delegate.h"
#include "chromeos/ash/services/bluetooth_config/fake_device_pairing_delegate.h"
#include "chromeos/ash/services/bluetooth_config/fake_device_pairing_handler.h"
#include "chromeos/ash/services/bluetooth_config/fake_discovered_devices_provider.h"
#include "chromeos/ash/services/bluetooth_config/fake_discovery_session_status_observer.h"
#include "device/bluetooth/floss/floss_features.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/bluetooth/test/mock_bluetooth_device.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.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>>;

using StartScanCallback = base::OnceCallback<void(
    /*is_error=*/bool,
    device::UMABluetoothDiscoverySessionOutcome)>;
using StopScanCallback =
    device::BluetoothAdapter::DiscoverySessionResultCallback;

using HandlerId = base::TokenType<class HandlerIdTag>;

const uint32_t kTestBluetoothClass = 1337u;
const char kTestBluetoothName[] = "testName";

}  // namespace

class DiscoverySessionManagerImplTest : public testing::Test {
 protected:
  DiscoverySessionManagerImplTest() = default;
  DiscoverySessionManagerImplTest(const DiscoverySessionManagerImplTest&) =
      delete;
  DiscoverySessionManagerImplTest& operator=(
      const DiscoverySessionManagerImplTest&) = delete;
  ~DiscoverySessionManagerImplTest() override = default;

  // testing::Test:
  void SetUp() override {
    DevicePairingHandlerImpl::Factory::SetFactoryForTesting(
        &fake_device_pairing_handler_factory_);

    mock_adapter_ =
        base::MakeRefCounted<testing::NiceMock<device::MockBluetoothAdapter>>();
    ON_CALL(*mock_adapter_, StartScanWithFilter_(testing::_, testing::_))
        .WillByDefault(testing::Invoke(
            [this](const device::BluetoothDiscoveryFilter* filter,
                   StartScanCallback& callback) {
              EXPECT_FALSE(start_scan_callback_);
              start_scan_callback_ = std::move(callback);

              if (should_synchronously_invoke_start_scan_callback_) {
                InvokePendingStartScanCallback(/*success=*/true);
              }
            }));
    ON_CALL(*mock_adapter_, StopScan(testing::_))
        .WillByDefault(testing::Invoke([this](StopScanCallback callback) {
          EXPECT_FALSE(stop_scan_callback_);
          stop_scan_callback_ = std::move(callback);
        }));

    discovery_session_manager_ = std::make_unique<DiscoverySessionManagerImpl>(
        &fake_adapter_state_controller_, mock_adapter_,
        &fake_discovered_devices_provider_, /*fast_pair_delegate=*/nullptr);
  }

  void TearDown() override {
    DevicePairingHandlerImpl::Factory::SetFactoryForTesting(nullptr);
  }

  std::unique_ptr<FakeBluetoothDiscoveryDelegate> StartDiscovery() {
    auto delegate = std::make_unique<FakeBluetoothDiscoveryDelegate>();
    discovery_session_manager_->StartDiscovery(
        delegate->GeneratePendingRemote());

    base::RunLoop().RunUntilIdle();
    EXPECT_TRUE(delegate->IsMojoPipeConnected());

    return delegate;
  }

  bool HasPendingStartScanCallback() const {
    return !start_scan_callback_.is_null();
  }

  void InvokePendingStartScanCallback(bool success) {
    std::move(start_scan_callback_)
        .Run(!success,
             success ? device::UMABluetoothDiscoverySessionOutcome::SUCCESS
                     : device::UMABluetoothDiscoverySessionOutcome::FAILED);
    base::RunLoop().RunUntilIdle();
  }

  bool HasPendingStopScanCallback() const {
    return !stop_scan_callback_.is_null();
  }

  void InvokePendingStopScanCallback(bool success) {
    std::move(stop_scan_callback_)
        .Run(!success,
             success ? device::UMABluetoothDiscoverySessionOutcome::SUCCESS
                     : device::UMABluetoothDiscoverySessionOutcome::FAILED);
    base::RunLoop().RunUntilIdle();
  }

  void SimulateDiscoverySessionStopping() {
    DiscoverySessionManagerImpl* impl =
        static_cast<DiscoverySessionManagerImpl*>(
            discovery_session_manager_.get());
    impl->AdapterDiscoveringChanged(mock_adapter_.get(), /*discovering=*/false);
    base::RunLoop().RunUntilIdle();
  }

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

  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);
    mock_devices_.push_back(std::move(mock_device));

    // Add the device to the discovered devices provider's discovered devices.
    std::vector<mojom::BluetoothDevicePropertiesPtr> discovered_devices;
    for (auto& device : mock_devices_) {
      discovered_devices.push_back(GenerateBluetoothDeviceMojoProperties(
          device.get(), /*fast_pair_delegate=*/nullptr));
    }
    fake_discovered_devices_provider_.SetDiscoveredDevices(
        std::move(discovered_devices));
    discovery_session_manager_->FlushForTesting();
  }

  void SetShouldSynchronouslyInvokeStartScanCallback(bool should) {
    should_synchronously_invoke_start_scan_callback_ = should;
  }

  std::unique_ptr<FakeDiscoverySessionStatusObserver> Observe() {
    auto observer = std::make_unique<FakeDiscoverySessionStatusObserver>();
    discovery_session_manager_->ObserveDiscoverySessionStatusChanges(
        observer->GeneratePendingRemote());
    discovery_session_manager_->FlushForTesting();
    return observer;
  }

  std::vector<raw_ptr<FakeDevicePairingHandler, VectorExperimental>>&
  GetDevicePairingHandlers() {
    return fake_device_pairing_handler_factory_.device_pairing_handlers();
  }

 private:
  class FakeDevicePairingHandlerFactory
      : public DevicePairingHandlerImpl::Factory {
   public:
    FakeDevicePairingHandlerFactory() = default;
    ~FakeDevicePairingHandlerFactory() override = default;

    std::vector<raw_ptr<FakeDevicePairingHandler, VectorExperimental>>&
    device_pairing_handlers() {
      return device_pairing_handlers_;
    }

   private:
    std::unique_ptr<DevicePairingHandler> CreateInstance(
        mojo::PendingReceiver<mojom::DevicePairingHandler> pending_receiver,
        AdapterStateController* adapter_state_controller,
        scoped_refptr<device::BluetoothAdapter> bluetooth_adapter,
        FastPairDelegate* fast_pair_delegate) override {
      EXPECT_TRUE(pending_receiver);
      EXPECT_TRUE(adapter_state_controller);
      EXPECT_TRUE(bluetooth_adapter);

      auto fake_device_pairing_handler =
          std::make_unique<FakeDevicePairingHandler>(
              std::move(pending_receiver), adapter_state_controller);
      device_pairing_handlers_.push_back(fake_device_pairing_handler.get());
      return fake_device_pairing_handler;
    }

    std::vector<raw_ptr<FakeDevicePairingHandler, VectorExperimental>>
        device_pairing_handlers_;
  };

  base::test::TaskEnvironment task_environment_;

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

  bool should_synchronously_invoke_start_scan_callback_ = false;
  StartScanCallback start_scan_callback_;
  StopScanCallback stop_scan_callback_;
  device::BluetoothDevice::ConnectCallback connect_callback_;

  FakeAdapterStateController fake_adapter_state_controller_;
  FakeDiscoveredDevicesProvider fake_discovered_devices_provider_;
  scoped_refptr<testing::NiceMock<device::MockBluetoothAdapter>> mock_adapter_;
  FakeDevicePairingHandlerFactory fake_device_pairing_handler_factory_;

  std::unique_ptr<DiscoverySessionManager> discovery_session_manager_;
};

TEST_F(DiscoverySessionManagerImplTest, StartDiscoveryThenDisconnectToStop) {
  std::unique_ptr<FakeBluetoothDiscoveryDelegate> delegate = StartDiscovery();

  InvokePendingStartScanCallback(/*success=*/true);
  EXPECT_TRUE(delegate->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate->num_start_callbacks());
  EXPECT_TRUE(delegate->discovered_devices_list().empty());

  // Add an unpaired device and verify that the delegate was notified.
  std::string device_id1;
  AddDevice(&device_id1);
  EXPECT_EQ(1u, delegate->discovered_devices_list().size());
  EXPECT_EQ(device_id1, delegate->discovered_devices_list()[0]->id);

  // Add another unpaired device and verify that the delegate was notified.
  std::string device_id2;
  AddDevice(&device_id2);
  EXPECT_EQ(2u, delegate->discovered_devices_list().size());
  EXPECT_EQ(device_id2, delegate->discovered_devices_list()[1]->id);

  // Disconnect the Mojo pipe; this should trigger a StopScan() call.
  delegate->DisconnectMojoPipe();
  EXPECT_FALSE(delegate->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate->num_start_callbacks());

  // Invoke the StopScan() callback. Since the delegate was already
  // disconnected, it should not have received a callback.
  InvokePendingStopScanCallback(/*success=*/true);
  EXPECT_EQ(0u, delegate->num_stop_callbacks());

  // Add another unpaired device; the delegate should not be notified.
  std::string device_id3;
  AddDevice(&device_id3);
  EXPECT_EQ(2u, delegate->discovered_devices_list().size());
  EXPECT_EQ(device_id1, delegate->discovered_devices_list()[0]->id);
}

TEST_F(DiscoverySessionManagerImplTest, FailToStart) {
  std::unique_ptr<FakeBluetoothDiscoveryDelegate> delegate = StartDiscovery();

  // Fail to start scanning.
  InvokePendingStartScanCallback(/*success=*/false);
  EXPECT_EQ(0u, delegate->num_start_callbacks());

  // We expect that another request was made to retry scanning; succeed this
  // time.
  InvokePendingStartScanCallback(/*success=*/true);
  EXPECT_EQ(1u, delegate->num_start_callbacks());
}

TEST_F(DiscoverySessionManagerImplTest, DisconnectBeforeFailureToStart) {
  std::unique_ptr<FakeBluetoothDiscoveryDelegate> delegate = StartDiscovery();

  // Disconnect the Mojo pipe, before the discovery session starts.
  delegate->DisconnectMojoPipe();
  EXPECT_FALSE(delegate->IsMojoPipeConnected());
  EXPECT_EQ(0u, delegate->num_start_callbacks());

  // Invoke the pending callback to start the scan. Since the delegate was
  // already disconnected, it should not have receivd a callback.
  InvokePendingStartScanCallback(/*success=*/true);
  EXPECT_EQ(0u, delegate->num_start_callbacks());

  // Since there is no longer a client, we should have attempted to stop the
  // discovery session. Invoke the StopScan() callback; since the delegate was
  // already disconnected, it should not have received a callback.
  InvokePendingStopScanCallback(/*success=*/true);
  EXPECT_EQ(0u, delegate->num_stop_callbacks());
}

TEST_F(DiscoverySessionManagerImplTest, UnexpectedlyStoppedDiscovering) {
  std::unique_ptr<FakeBluetoothDiscoveryDelegate> delegate = StartDiscovery();

  InvokePendingStartScanCallback(/*success=*/true);
  EXPECT_TRUE(delegate->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate->num_start_callbacks());

  // Simulate the discovery session stopping unexpectedly. The delegate should
  // become disconnected.
  SimulateDiscoverySessionStopping();
  EXPECT_FALSE(delegate->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate->num_stop_callbacks());
}

TEST_F(DiscoverySessionManagerImplTest, BluetoothTurnsOff) {
  std::unique_ptr<FakeBluetoothDiscoveryDelegate> delegate = StartDiscovery();

  InvokePendingStartScanCallback(/*success=*/true);
  EXPECT_TRUE(delegate->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate->num_start_callbacks());

  // Start disabling Bluetooth; the delegate should become disconnected.
  SetBluetoothSystemState(mojom::BluetoothSystemState::kDisabling);
  EXPECT_FALSE(delegate->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate->num_stop_callbacks());
}

TEST_F(DiscoverySessionManagerImplTest, MultipleClients) {
  // Add the first client and start discovery.
  std::unique_ptr<FakeBluetoothDiscoveryDelegate> delegate1 = StartDiscovery();
  InvokePendingStartScanCallback(/*success=*/true);
  EXPECT_TRUE(delegate1->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate1->num_start_callbacks());
  EXPECT_TRUE(delegate1->discovered_devices_list().empty());

  // Add an unpaired device and verify that the first client was notified.
  std::string device_id1;
  AddDevice(&device_id1);
  EXPECT_EQ(1u, delegate1->discovered_devices_list().size());
  EXPECT_EQ(device_id1, delegate1->discovered_devices_list()[0]->id);

  // Add a second client; it should reuse the existing discovery session, and no
  // new pending request should have been created. It should immediately be
  // notified of the current discovered devices list.
  std::unique_ptr<FakeBluetoothDiscoveryDelegate> delegate2 = StartDiscovery();
  EXPECT_TRUE(delegate2->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate2->num_start_callbacks());
  EXPECT_FALSE(HasPendingStartScanCallback());
  EXPECT_EQ(1u, delegate2->discovered_devices_list().size());
  EXPECT_EQ(device_id1, delegate2->discovered_devices_list()[0]->id);

  // Disconnect the first client; since the second client is still active, there
  // should be no pending StopScan() call.
  delegate1->DisconnectMojoPipe();
  EXPECT_FALSE(HasPendingStopScanCallback());

  // Add another unpaired device; the second client should be notified but the
  // first client should not.
  std::string device_id2;
  AddDevice(&device_id2);
  EXPECT_EQ(1u, delegate1->discovered_devices_list().size());
  EXPECT_EQ(device_id1, delegate1->discovered_devices_list()[0]->id);
  EXPECT_EQ(2u, delegate2->discovered_devices_list().size());
  EXPECT_EQ(device_id2, delegate2->discovered_devices_list()[1]->id);

  // Disconnect the second client; now that there are no remaining clients,
  // StopScan() should have been called.
  delegate2->DisconnectMojoPipe();
  InvokePendingStopScanCallback(/*success=*/true);
}

TEST_F(DiscoverySessionManagerImplTest, DiscoverDeviceBeforeStart) {
  // Add an unpaired device.
  std::string device_id;
  AddDevice(&device_id);

  // Add a client and start discovery. The client should be notified of the
  // current discovered devices list once discovery has started.
  std::unique_ptr<FakeBluetoothDiscoveryDelegate> delegate = StartDiscovery();
  InvokePendingStartScanCallback(/*success=*/true);
  EXPECT_TRUE(delegate->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate->discovered_devices_list().size());
  EXPECT_EQ(device_id, delegate->discovered_devices_list()[0]->id);

  delegate->DisconnectMojoPipe();
  InvokePendingStopScanCallback(/*success=*/true);
}

TEST_F(DiscoverySessionManagerImplTest, MultipleClientsAttemptPairing) {
  std::unique_ptr<FakeDiscoverySessionStatusObserver> observer = Observe();

  // Initially, observer would see the default state, which is 0 sessions.
  EXPECT_FALSE(observer->has_at_least_one_discovery_session());
  EXPECT_EQ(0, observer->num_discovery_session_changed_calls());

  std::unique_ptr<FakeBluetoothDiscoveryDelegate> delegate1 = StartDiscovery();
  InvokePendingStartScanCallback(/*success=*/true);
  EXPECT_TRUE(delegate1->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate1->num_start_callbacks());
  EXPECT_TRUE(delegate1->pairing_handler().is_connected());
  // Going from 0 to 1 discovery sessions should notify observers.
  EXPECT_TRUE(observer->has_at_least_one_discovery_session());
  EXPECT_EQ(1, observer->num_discovery_session_changed_calls());

  std::unique_ptr<FakeBluetoothDiscoveryDelegate> delegate2 = StartDiscovery();
  EXPECT_TRUE(delegate2->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate2->num_start_callbacks());
  EXPECT_TRUE(delegate2->pairing_handler().is_connected());
  // Going from 1 to 1+ discovery sessions should not notify observers.
  EXPECT_TRUE(observer->has_at_least_one_discovery_session());
  EXPECT_EQ(1, observer->num_discovery_session_changed_calls());

  // Simulate first client attempting to pair with an unknown device_id.
  EXPECT_EQ(2u, GetDevicePairingHandlers().size());
  FakeDevicePairingHandler* device_pairing_handler1 =
      GetDevicePairingHandlers()[0];
  EXPECT_TRUE(device_pairing_handler1->current_pairing_device_id().empty());
  std::optional<mojom::PairingResult> result;
  auto pairing_delegate1 = std::make_unique<FakeDevicePairingDelegate>();
  const std::string device_id = "device_id";
  delegate1->pairing_handler()->PairDevice(
      device_id, pairing_delegate1->GeneratePendingRemote(),
      base::BindLambdaForTesting(
          [&result](mojom::PairingResult pairing_result) {
            result = pairing_result;
          }));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(device_id, device_pairing_handler1->current_pairing_device_id());

  // Finish the pairing with failure.
  device_pairing_handler1->SimulatePairDeviceFinished(
      device::ConnectionFailureReason::kFailed);
  EXPECT_EQ(result, mojom::PairingResult::kNonAuthFailure);
  EXPECT_TRUE(delegate1->IsMojoPipeConnected());
  EXPECT_TRUE(delegate1->pairing_handler().is_connected());
  EXPECT_TRUE(pairing_delegate1->IsMojoPipeConnected());

  // First client pairs with a known device_id.
  EXPECT_TRUE(device_pairing_handler1->current_pairing_device_id().empty());
  std::string device_id1;
  AddDevice(&device_id1);
  auto pairing_delegate2 = std::make_unique<FakeDevicePairingDelegate>();
  delegate1->pairing_handler()->PairDevice(
      device_id1, pairing_delegate2->GeneratePendingRemote(),
      base::BindLambdaForTesting(
          [&result](mojom::PairingResult pairing_result) {
            result = pairing_result;
          }));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(device_id1, device_pairing_handler1->current_pairing_device_id());

  // Finish the pairing with success.
  device_pairing_handler1->SimulatePairDeviceFinished(
      /*failure_reason=*/std::nullopt);
  EXPECT_EQ(result, mojom::PairingResult::kSuccess);
  EXPECT_TRUE(delegate1->IsMojoPipeConnected());
  EXPECT_TRUE(delegate1->pairing_handler().is_connected());
  EXPECT_TRUE(pairing_delegate2->IsMojoPipeConnected());

  // Disconnect the first client.
  delegate1->DisconnectMojoPipe();

  // Going from 1+ to 1 discovery sessions should not notify observers.
  EXPECT_TRUE(observer->has_at_least_one_discovery_session());
  EXPECT_EQ(1, observer->num_discovery_session_changed_calls());

  // Simulate second client pairing with a known device_id.
  EXPECT_EQ(2u, GetDevicePairingHandlers().size());
  FakeDevicePairingHandler* device_pairing_handler2 =
      GetDevicePairingHandlers()[1];
  EXPECT_TRUE(device_pairing_handler2->current_pairing_device_id().empty());
  std::string device_id2;
  AddDevice(&device_id2);
  auto pairing_delegate3 = std::make_unique<FakeDevicePairingDelegate>();
  delegate2->pairing_handler()->PairDevice(
      device_id2, pairing_delegate3->GeneratePendingRemote(),
      base::BindLambdaForTesting(
          [&result](mojom::PairingResult pairing_result) {
            result = pairing_result;
          }));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(device_id2, device_pairing_handler2->current_pairing_device_id());

  // Finish the pairing with success.
  device_pairing_handler2->SimulatePairDeviceFinished(
      /*failure_reason=*/std::nullopt);
  EXPECT_EQ(result, mojom::PairingResult::kSuccess);
  EXPECT_TRUE(delegate2->IsMojoPipeConnected());
  EXPECT_TRUE(delegate2->pairing_handler().is_connected());
  EXPECT_TRUE(pairing_delegate3->IsMojoPipeConnected());

  // Disconnect the second client.
  delegate2->DisconnectMojoPipe();
  InvokePendingStopScanCallback(/*success=*/true);

  // Going from 1 to 0 discovery sessions should notify observers.
  EXPECT_FALSE(observer->has_at_least_one_discovery_session());
  EXPECT_EQ(2, observer->num_discovery_session_changed_calls());
}

TEST_F(DiscoverySessionManagerImplTest, StartDiscoverySynchronous) {
  // Simulate adapter finishing starting scanning immediately. This should cause
  // |delegate.OnBluetoothDiscoveryStarted()| still to only be called once.
  SetShouldSynchronouslyInvokeStartScanCallback(true);
  std::unique_ptr<FakeBluetoothDiscoveryDelegate> delegate = StartDiscovery();
  EXPECT_TRUE(delegate->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate->num_start_callbacks());
  EXPECT_TRUE(delegate->discovered_devices_list().empty());

  // Disconnect the Mojo pipe; this should trigger a StopScan() call.
  delegate->DisconnectMojoPipe();
  EXPECT_FALSE(delegate->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate->num_start_callbacks());

  // Invoke the StopScan() callback. Since the delegate was already
  // disconnected, it should not have received a callback.
  InvokePendingStopScanCallback(/*success=*/true);
  EXPECT_EQ(0u, delegate->num_stop_callbacks());
}

TEST_F(DiscoverySessionManagerImplTest, DisconnectToStopObserving) {
  std::unique_ptr<FakeDiscoverySessionStatusObserver> observer = Observe();

  // Initially, observer would see the default state, which is 0 sessions.
  EXPECT_FALSE(observer->has_at_least_one_discovery_session());
  EXPECT_EQ(0, observer->num_discovery_session_changed_calls());

  // Disconnect the Mojo pipe; this should stop observing.
  observer->DisconnectMojoPipe();

  // Add the first client and start discovery. The observer should not be
  // notified since it is disconnected.
  std::unique_ptr<FakeBluetoothDiscoveryDelegate> delegate1 = StartDiscovery();
  InvokePendingStartScanCallback(/*success=*/true);
  EXPECT_FALSE(observer->has_at_least_one_discovery_session());
  EXPECT_EQ(0, observer->num_discovery_session_changed_calls());
}

TEST_F(DiscoverySessionManagerImplTest, AdapterDiscoveringStopsDuringPairing) {
  std::unique_ptr<FakeBluetoothDiscoveryDelegate> delegate = StartDiscovery();

  InvokePendingStartScanCallback(/*success=*/true);
  EXPECT_TRUE(delegate->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate->num_start_callbacks());
  EXPECT_TRUE(delegate->pairing_handler().is_connected());

  // Simulate client pairing with a device.
  EXPECT_EQ(1u, GetDevicePairingHandlers().size());
  FakeDevicePairingHandler* device_pairing_handler =
      GetDevicePairingHandlers()[0];
  EXPECT_TRUE(device_pairing_handler->current_pairing_device_id().empty());
  std::string device_id;
  AddDevice(&device_id);
  std::optional<mojom::PairingResult> result;
  auto pairing_delegate = std::make_unique<FakeDevicePairingDelegate>();
  delegate->pairing_handler()->PairDevice(
      device_id, pairing_delegate->GeneratePendingRemote(),
      base::BindLambdaForTesting(
          [&result](mojom::PairingResult pairing_result) {
            result = pairing_result;
          }));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(device_id, device_pairing_handler->current_pairing_device_id());

  // Simulate the discovery session stopping unexpectedly before pairing
  // completes. Discovery should stop and the discovery delegate, pairing
  // handler, and pairing delegate disconnected.
  SimulateDiscoverySessionStopping();
  EXPECT_FALSE(delegate->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate->num_stop_callbacks());
  EXPECT_FALSE(delegate->pairing_handler().is_connected());
  EXPECT_FALSE(pairing_delegate->IsMojoPipeConnected());
  InvokePendingStopScanCallback(/*success=*/true);
}

TEST_F(DiscoverySessionManagerImplTest,
       AdapterDiscoveringStopsDuringPairing_Floss) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(floss::features::kFlossEnabled);
  EXPECT_TRUE(floss::features::IsFlossEnabled());

  std::unique_ptr<FakeBluetoothDiscoveryDelegate> delegate = StartDiscovery();

  InvokePendingStartScanCallback(/*success=*/true);
  EXPECT_TRUE(delegate->IsMojoPipeConnected());
  EXPECT_EQ(1u, delegate->num_start_callbacks());
  EXPECT_TRUE(delegate->pairing_handler().is_connected());

  // Simulate client pairing with a device.
  EXPECT_EQ(1u, GetDevicePairingHandlers().size());
  FakeDevicePairingHandler* device_pairing_handler =
      GetDevicePairingHandlers()[0];
  EXPECT_TRUE(device_pairing_handler->current_pairing_device_id().empty());
  std::string device_id;
  AddDevice(&device_id);
  std::optional<mojom::PairingResult> result;
  auto pairing_delegate = std::make_unique<FakeDevicePairingDelegate>();
  delegate->pairing_handler()->PairDevice(
      device_id, pairing_delegate->GeneratePendingRemote(),
      base::BindLambdaForTesting(
          [&result](mojom::PairingResult pairing_result) {
            result = pairing_result;
          }));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(device_id, device_pairing_handler->current_pairing_device_id());

  // Simulate the discovery session stopping before pairing
  // completes. This can happen with Floss enabled, but the device pairing
  // handler should remain alive.
  // TODO(b/222230887): Remove this test when DiscoverySessionManager and
  // DevicePairingHandler lifecycles are decoupled.
  SimulateDiscoverySessionStopping();
  EXPECT_TRUE(delegate->IsMojoPipeConnected());
  EXPECT_EQ(0u, delegate->num_stop_callbacks());
  EXPECT_TRUE(delegate->pairing_handler().is_connected());
  EXPECT_TRUE(pairing_delegate->IsMojoPipeConnected());

  device_pairing_handler->SimulatePairDeviceFinished(
      /*failure_reason=*/std::nullopt);
  EXPECT_EQ(result, mojom::PairingResult::kSuccess);

  // |delegate| will still be connected.
  EXPECT_TRUE(delegate->pairing_handler().is_connected());
  EXPECT_TRUE(delegate->IsMojoPipeConnected());
  EXPECT_TRUE(delegate->pairing_handler().is_connected());
  EXPECT_TRUE(pairing_delegate->IsMojoPipeConnected());
}

}  // namespace ash::bluetooth_config