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

#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "device/bluetooth/chromeos/bluetooth_utils.h"
#include "device/bluetooth/floss/floss_features.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.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 kTestIdBluez[] = "/org/bluez/hci0/dev_7C_96_D2_8B_FB_17";
const char kTestIdFloss0[] = "7C:96:D2:8B:FB:17";
const char kTestIdFloss1[] = "C7:69:2D:B8:BF:71";
const char kTestNickname[] = "nickname";

class FakeObserver : public DeviceNameManager::Observer {
 public:
  FakeObserver() = default;
  ~FakeObserver() override = default;

  size_t num_device_nickname_changed_calls() const {
    return num_device_nickname_changed_calls_;
  }

  const std::string& last_device_id_nickname_changed() const {
    return last_device_id_nickname_changed_;
  }

  const std::optional<std::string>& last_device_nickname_changed() const {
    return last_device_nickname_changed_;
  }

 private:
  // DeviceNameManager::Observer:
  void OnDeviceNicknameChanged(
      const std::string& device_id,
      const std::optional<std::string>& nickname) override {
    ++num_device_nickname_changed_calls_;
    last_device_id_nickname_changed_ = device_id;
    last_device_nickname_changed_ = nickname;
  }

  size_t num_device_nickname_changed_calls_ = 0u;
  std::string last_device_id_nickname_changed_;
  std::optional<std::string> last_device_nickname_changed_;
};

}  // namespace

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

  // testing::Test:
  void SetUp() override {
    DeviceNameManagerImpl::RegisterLocalStatePrefs(
        test_pref_service_.registry());

    mock_adapter_ =
        base::MakeRefCounted<testing::NiceMock<device::MockBluetoothAdapter>>();
    ON_CALL(*mock_adapter_, GetDevices())
        .WillByDefault(
            testing::Invoke(this, &DeviceNameManagerImplTest::GetMockDevices));
  }

  std::unique_ptr<DeviceNameManagerImpl> CreateDeviceNameManager() {
    auto device_name_manager =
        std::make_unique<DeviceNameManagerImpl>(mock_adapter_);
    device_name_manager->AddObserver(&fake_observer_);
    device_name_manager->SetPrefs(&test_pref_service_);
    return device_name_manager;
  }

  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));
  }

  size_t GetNumDeviceNicknameObserverEvents() const {
    return fake_observer_.num_device_nickname_changed_calls();
  }

  const std::string& GetLastDeviceIdNicknameChanged() const {
    return fake_observer_.last_device_id_nickname_changed();
  }

  const std::optional<std::string>& GetLastDeviceNicknameChanged() const {
    return fake_observer_.last_device_nickname_changed();
  }

  sync_preferences::TestingPrefServiceSyncable* local_state() {
    return &test_pref_service_;
  }

  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_;

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

  scoped_refptr<testing::NiceMock<device::MockBluetoothAdapter>> mock_adapter_;
  sync_preferences::TestingPrefServiceSyncable test_pref_service_;
  FakeObserver fake_observer_;
};

TEST_F(DeviceNameManagerImplTest, GetThenSetValidThenSetInvalidFlossDisabled) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(floss::features::kFlossEnabled);
  EXPECT_FALSE(floss::features::IsFlossEnabled());

  std::string device_id;
  AddDevice(&device_id);
  std::unique_ptr<DeviceNameManagerImpl> manager = CreateDeviceNameManager();
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));

  manager->SetDeviceNickname(device_id, kTestNickname);
  EXPECT_EQ(manager->GetDeviceNickname(device_id), kTestNickname);
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 1u);
  EXPECT_EQ(GetLastDeviceIdNicknameChanged(), device_id);
  EXPECT_EQ(GetLastDeviceNicknameChanged(), kTestNickname);

  histogram_tester.ExpectBucketCount(
      "Bluetooth.ChromeOS.SetNickname.Result",
      device::SetNicknameResult::kInvalidNicknameFormat, 0);
  histogram_tester.ExpectBucketCount("Bluetooth.ChromeOS.SetNickname.Result",
                                     device::SetNicknameResult::kSuccess, 1);

  // Set an empty nickname, this should fail and the nickname should be
  // unchanged.
  manager->SetDeviceNickname(device_id, "");
  EXPECT_EQ(manager->GetDeviceNickname(device_id), kTestNickname);
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 1u);
  histogram_tester.ExpectBucketCount(
      "Bluetooth.ChromeOS.SetNickname.Result",
      device::SetNicknameResult::kInvalidNicknameFormat, 1);
  histogram_tester.ExpectBucketCount("Bluetooth.ChromeOS.SetNickname.Result",
                                     device::SetNicknameResult::kSuccess, 1);

  // Set nickname above character limit, this should also fail and the nickname
  // should be unchanged.
  manager->SetDeviceNickname(device_id, "123456789012345678901234567890123");
  EXPECT_EQ(manager->GetDeviceNickname(device_id), kTestNickname);
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 1u);

  histogram_tester.ExpectBucketCount(
      "Bluetooth.ChromeOS.SetNickname.Result",
      device::SetNicknameResult::kInvalidNicknameFormat, 2);
  histogram_tester.ExpectBucketCount("Bluetooth.ChromeOS.SetNickname.Result",
                                     device::SetNicknameResult::kSuccess, 1);
}

TEST_F(DeviceNameManagerImplTest,
       NicknameIsPersistedBetweenManagerInstancesFlossDisabled) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(floss::features::kFlossEnabled);
  EXPECT_FALSE(floss::features::IsFlossEnabled());

  std::string device_id;
  AddDevice(&device_id);
  std::unique_ptr<DeviceNameManagerImpl> manager = CreateDeviceNameManager();
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));

  manager->SetDeviceNickname(device_id, kTestNickname);
  EXPECT_EQ(manager->GetDeviceNickname(device_id), kTestNickname);
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 1u);
  EXPECT_EQ(GetLastDeviceIdNicknameChanged(), device_id);
  EXPECT_EQ(GetLastDeviceNicknameChanged(), kTestNickname);
  histogram_tester.ExpectBucketCount(
      "Bluetooth.ChromeOS.SetNickname.Result",
      device::SetNicknameResult::kInvalidNicknameFormat, 0);
  histogram_tester.ExpectBucketCount("Bluetooth.ChromeOS.SetNickname.Result",
                                     device::SetNicknameResult::kSuccess, 1);

  // Create a new manager and destroy the old one.
  manager = CreateDeviceNameManager();
  EXPECT_EQ(manager->GetDeviceNickname(device_id), kTestNickname);
}

TEST_F(DeviceNameManagerImplTest, SetNicknameDeviceNotFoundFlossDisabled) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(floss::features::kFlossEnabled);
  EXPECT_FALSE(floss::features::IsFlossEnabled());

  const std::string device_id = "device_id";
  std::unique_ptr<DeviceNameManagerImpl> manager = CreateDeviceNameManager();
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));

  // Setting the nickname of an unknown device should fail.
  manager->SetDeviceNickname(device_id, kTestNickname);
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 0u);
  histogram_tester.ExpectBucketCount("Bluetooth.ChromeOS.SetNickname.Result",
                                     device::SetNicknameResult::kDeviceNotFound,
                                     1);
  histogram_tester.ExpectBucketCount("Bluetooth.ChromeOS.SetNickname.Result",
                                     device::SetNicknameResult::kSuccess, 0);
}

TEST_F(DeviceNameManagerImplTest, RemoveThenSetThenRemoveFlossDisabled) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(floss::features::kFlossEnabled);
  EXPECT_FALSE(floss::features::IsFlossEnabled());

  std::string device_id;
  AddDevice(&device_id);
  std::unique_ptr<DeviceNameManagerImpl> manager = CreateDeviceNameManager();
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));

  // Nothing should happen when removing a nickname that doesn't exist.
  manager->RemoveDeviceNickname(device_id);
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 0u);
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));

  manager->SetDeviceNickname(device_id, kTestNickname);
  EXPECT_EQ(manager->GetDeviceNickname(device_id), kTestNickname);
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 1u);
  EXPECT_EQ(GetLastDeviceIdNicknameChanged(), device_id);
  EXPECT_EQ(GetLastDeviceNicknameChanged(), kTestNickname);

  manager->RemoveDeviceNickname(device_id);
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 2u);
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));
  EXPECT_EQ(GetLastDeviceIdNicknameChanged(), device_id);
  EXPECT_EQ(GetLastDeviceNicknameChanged(), std::nullopt);

  // Create a new manager and destroy the old one.
  manager = CreateDeviceNameManager();
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));
}

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

  std::string device_id;
  AddDevice(&device_id);
  std::unique_ptr<DeviceNameManagerImpl> manager = CreateDeviceNameManager();
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));

  manager->SetDeviceNickname(device_id, kTestNickname);
  EXPECT_EQ(manager->GetDeviceNickname(device_id), kTestNickname);
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 1u);
  EXPECT_EQ(GetLastDeviceIdNicknameChanged(), device_id);
  EXPECT_EQ(GetLastDeviceNicknameChanged(), kTestNickname);

  histogram_tester.ExpectBucketCount(
      "Bluetooth.ChromeOS.SetNickname.Result",
      device::SetNicknameResult::kInvalidNicknameFormat, 0);
  histogram_tester.ExpectBucketCount("Bluetooth.ChromeOS.SetNickname.Result",
                                     device::SetNicknameResult::kSuccess, 1);

  // Set an empty nickname, this should fail and the nickname should be
  // unchanged.
  manager->SetDeviceNickname(device_id, "");
  EXPECT_EQ(manager->GetDeviceNickname(device_id), kTestNickname);
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 1u);
  histogram_tester.ExpectBucketCount(
      "Bluetooth.ChromeOS.SetNickname.Result",
      device::SetNicknameResult::kInvalidNicknameFormat, 1);
  histogram_tester.ExpectBucketCount("Bluetooth.ChromeOS.SetNickname.Result",
                                     device::SetNicknameResult::kSuccess, 1);

  // Set nickname above character limit, this should also fail and the nickname
  // should be unchanged.
  manager->SetDeviceNickname(device_id, "123456789012345678901234567890123");
  EXPECT_EQ(manager->GetDeviceNickname(device_id), kTestNickname);
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 1u);

  histogram_tester.ExpectBucketCount(
      "Bluetooth.ChromeOS.SetNickname.Result",
      device::SetNicknameResult::kInvalidNicknameFormat, 2);
  histogram_tester.ExpectBucketCount("Bluetooth.ChromeOS.SetNickname.Result",
                                     device::SetNicknameResult::kSuccess, 1);
}

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

  std::string device_id;
  AddDevice(&device_id);
  std::unique_ptr<DeviceNameManagerImpl> manager = CreateDeviceNameManager();
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));

  manager->SetDeviceNickname(device_id, kTestNickname);
  EXPECT_EQ(manager->GetDeviceNickname(device_id), kTestNickname);
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 1u);
  EXPECT_EQ(GetLastDeviceIdNicknameChanged(), device_id);
  EXPECT_EQ(GetLastDeviceNicknameChanged(), kTestNickname);
  histogram_tester.ExpectBucketCount(
      "Bluetooth.ChromeOS.SetNickname.Result",
      device::SetNicknameResult::kInvalidNicknameFormat, 0);
  histogram_tester.ExpectBucketCount("Bluetooth.ChromeOS.SetNickname.Result",
                                     device::SetNicknameResult::kSuccess, 1);

  // Create a new manager and destroy the old one.
  manager = CreateDeviceNameManager();
  EXPECT_EQ(manager->GetDeviceNickname(device_id), kTestNickname);
}

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

  const std::string device_id = "device_id";
  std::unique_ptr<DeviceNameManagerImpl> manager = CreateDeviceNameManager();
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));

  // Setting the nickname of an unknown device should fail.
  manager->SetDeviceNickname(device_id, kTestNickname);
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 0u);
  histogram_tester.ExpectBucketCount("Bluetooth.ChromeOS.SetNickname.Result",
                                     device::SetNicknameResult::kDeviceNotFound,
                                     1);
  histogram_tester.ExpectBucketCount("Bluetooth.ChromeOS.SetNickname.Result",
                                     device::SetNicknameResult::kSuccess, 0);
}

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

  std::string device_id;
  AddDevice(&device_id);
  std::unique_ptr<DeviceNameManagerImpl> manager = CreateDeviceNameManager();
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));

  // Nothing should happen when removing a nickname that doesn't exist.
  manager->RemoveDeviceNickname(device_id);
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 0u);
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));

  manager->SetDeviceNickname(device_id, kTestNickname);
  EXPECT_EQ(manager->GetDeviceNickname(device_id), kTestNickname);
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 1u);
  EXPECT_EQ(GetLastDeviceIdNicknameChanged(), device_id);
  EXPECT_EQ(GetLastDeviceNicknameChanged(), kTestNickname);

  manager->RemoveDeviceNickname(device_id);
  EXPECT_EQ(GetNumDeviceNicknameObserverEvents(), 2u);
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));
  EXPECT_EQ(GetLastDeviceIdNicknameChanged(), device_id);
  EXPECT_EQ(GetLastDeviceNicknameChanged(), std::nullopt);

  // Create a new manager and destroy the old one.
  manager = CreateDeviceNameManager();
  EXPECT_FALSE(manager->GetDeviceNickname(device_id));
}

TEST_F(DeviceNameManagerImplTest, Migration_DisablingClearsPrefs) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(floss::features::kFlossEnabled);
  EXPECT_FALSE(floss::features::IsFlossEnabled());

  // Set the pref to some arbitrary value since we just want to confirm it will
  // be cleared when the feature flag is disabled.
  local_state()->Set(DeviceNameManager::kDeviceIdToNicknameMapPrefName,
                     base::Value(base::Value::Dict()));
  EXPECT_TRUE(local_state()->HasPrefPath(
      DeviceNameManager::kDeviceIdToNicknameMapPrefName));
  CreateDeviceNameManager();
  EXPECT_FALSE(local_state()->HasPrefPath(
      DeviceNameManager::kDeviceIdToNicknameMapPrefName));
}

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

  // The value that we will set the existing/pre-migration prefs to.
  auto existing_prefs = base::Value::Dict().Set(kTestIdBluez, kTestNickname);
  local_state()->Set(DeviceNameManager::kDeviceIdToNicknameMapPrefNameLegacy,
                     base::Value(existing_prefs.Clone()));

  // Set the pref to some arbitrary value since we just want to confirm that if
  // the pref has a value we will assume that we have already performed the
  // migration and will not attempt another migration.
  base::Value::Dict new_prefs;
  local_state()->Set(DeviceNameManager::kDeviceIdToNicknameMapPrefName,
                     base::Value(new_prefs.Clone()));

  EXPECT_TRUE(local_state()->HasPrefPath(
      DeviceNameManager::kDeviceIdToNicknameMapPrefName));

  CreateDeviceNameManager();

  const base::Value::Dict& migrated_prefs =
      local_state()->GetDict(DeviceNameManager::kDeviceIdToNicknameMapPrefName);
  EXPECT_EQ(new_prefs, migrated_prefs);
}

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

  // The value that we expect to be migrated.
  auto existing_prefs = base::Value::Dict()
                            .Set(kTestIdBluez, kTestNickname)
                            .Set(kTestIdFloss1, kTestNickname);

  local_state()->Set(DeviceNameManager::kDeviceIdToNicknameMapPrefNameLegacy,
                     base::Value(existing_prefs.Clone()));

  EXPECT_FALSE(local_state()->HasPrefPath(
      DeviceNameManager::kDeviceIdToNicknameMapPrefName));

  CreateDeviceNameManager();

  auto migrated_prefs = base::Value::Dict()
                            .Set(kTestIdFloss0, kTestNickname)
                            .Set(kTestIdFloss1, kTestNickname);
  EXPECT_EQ(migrated_prefs,
            local_state()->GetDict(
                DeviceNameManager::kDeviceIdToNicknameMapPrefName));
}

}  // namespace ash::bluetooth_config