chromium/chromeos/ash/services/device_sync/cryptauth_device_manager_impl_unittest.cc

// Copyright 2018 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/device_sync/cryptauth_device_manager_impl.h"

#include <stddef.h>

#include <memory>
#include <utility>

#include "base/base64url.h"
#include "base/containers/contains.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/gmock_move_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/simple_test_clock.h"
#include "chromeos/ash/components/multidevice/software_feature_state.h"
#include "chromeos/ash/services/device_sync/fake_cryptauth_gcm_manager.h"
#include "chromeos/ash/services/device_sync/mock_cryptauth_client.h"
#include "chromeos/ash/services/device_sync/mock_sync_scheduler.h"
#include "chromeos/ash/services/device_sync/network_request_error.h"
#include "chromeos/ash/services/device_sync/pref_names.h"
#include "chromeos/ash/services/device_sync/proto/enum_util.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/prefs/testing_pref_service.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::_;
using ::testing::DoAll;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::SaveArg;

namespace ash {

namespace device_sync {

namespace {

// The initial "Now" time for testing.
const double kInitialTimeNowSeconds = 20000000;

// A later "Now" time for testing.
const double kLaterTimeNowSeconds = kInitialTimeNowSeconds + 30;

// The timestamp of a last successful sync in seconds.
const double kLastSyncTimeSeconds = kInitialTimeNowSeconds - (60 * 60 * 5);

// Unlock key fields originally stored in the user prefs.
const char kStoredPublicKey[] = "storedPublicKey";
const char kStoredDeviceName[] = "Pixel 2";
const char kStoredBluetoothAddress[] = "12:34:56:78:90:AB";
const bool kStoredUnlockable = false;

// cryptauth::ExternalDeviceInfo fields for the synced unlock key.
const char kPublicKey1[] = "GOOG";
const char kDeviceName1[] = "Pixel XL";
const char kNoPiiDeviceName1[] = "marlin";
const char kBluetoothAddress1[] = "aa:bb:cc:ee:dd:ff";
const bool kUnlockable1 = false;
const char kBeaconSeed1Data[] = "beaconSeed1Data";
const int64_t kBeaconSeed1StartTime = 123456;
const int64_t kBeaconSeed1EndTime = 123457;
const char kBeaconSeed2Data[] = "beaconSeed2Data";
const int64_t kBeaconSeed2StartTime = 234567;
const int64_t kBeaconSeed2EndTime = 234568;
const bool kArcPlusPlus1 = true;
const bool kPixelPhone1 = true;

// cryptauth::ExternalDeviceInfo fields for a non-synced unlockable device.
const char kPublicKey2[] = "CROS";
const char kDeviceName2[] = "Pixelbook";
const char kNoPiiDeviceName2[] = "eve-signed-mpkeys";
const bool kUnlockable2 = true;
const char kBeaconSeed3Data[] = "beaconSeed3Data";
const int64_t kBeaconSeed3StartTime = 123456;
const int64_t kBeaconSeed3EndTime = 123457;
const char kBeaconSeed4Data[] = "beaconSeed4Data";
const int64_t kBeaconSeed4StartTime = 234567;
const int64_t kBeaconSeed4EndTime = 234568;
const bool kArcPlusPlus2 = false;
const bool kPixelPhone2 = false;

// Validates that |devices| is equal to |expected_devices|.
void ExpectSyncedDevicesAreEqual(
    const std::vector<cryptauth::ExternalDeviceInfo>& expected_devices,
    const std::vector<cryptauth::ExternalDeviceInfo>& devices) {
  ASSERT_EQ(expected_devices.size(), devices.size());
  for (size_t i = 0; i < devices.size(); ++i) {
    SCOPED_TRACE(
        base::StringPrintf("Compare protos at index=%d", static_cast<int>(i)));
    const auto& expected_device = expected_devices[i];
    const auto& device = devices.at(i);
    EXPECT_TRUE(expected_device.has_public_key());
    EXPECT_TRUE(device.has_public_key());
    EXPECT_EQ(expected_device.public_key(), device.public_key());

    EXPECT_EQ(expected_device.has_friendly_device_name(),
              device.has_friendly_device_name());
    EXPECT_EQ(expected_device.friendly_device_name(),
              device.friendly_device_name());

    EXPECT_EQ(expected_device.has_no_pii_device_name(),
              device.has_no_pii_device_name());
    EXPECT_EQ(expected_device.no_pii_device_name(),
              device.no_pii_device_name());

    EXPECT_EQ(expected_device.has_bluetooth_address(),
              device.has_bluetooth_address());
    EXPECT_EQ(expected_device.bluetooth_address(), device.bluetooth_address());

    EXPECT_EQ(expected_device.has_unlock_key(), device.has_unlock_key());
    EXPECT_EQ(expected_device.unlock_key(), device.unlock_key());

    EXPECT_EQ(expected_device.has_unlockable(), device.has_unlockable());
    EXPECT_EQ(expected_device.unlockable(), device.unlockable());

    EXPECT_EQ(expected_device.has_last_update_time_millis(),
              device.has_last_update_time_millis());
    EXPECT_EQ(expected_device.last_update_time_millis(),
              device.last_update_time_millis());

    EXPECT_EQ(expected_device.has_mobile_hotspot_supported(),
              device.has_mobile_hotspot_supported());
    EXPECT_EQ(expected_device.mobile_hotspot_supported(),
              device.mobile_hotspot_supported());

    EXPECT_EQ(expected_device.has_device_type(), device.has_device_type());
    EXPECT_EQ(expected_device.device_type(), device.device_type());

    ASSERT_EQ(expected_device.beacon_seeds_size(), device.beacon_seeds_size());
    for (int beacon_seed = 0; beacon_seed < expected_device.beacon_seeds_size();
         beacon_seed++) {
      const cryptauth::BeaconSeed expected_seed =
          expected_device.beacon_seeds(beacon_seed);
      const cryptauth::BeaconSeed seed = device.beacon_seeds(beacon_seed);
      EXPECT_TRUE(expected_seed.has_data());
      EXPECT_TRUE(seed.has_data());
      EXPECT_EQ(expected_seed.data(), seed.data());

      EXPECT_TRUE(expected_seed.has_start_time_millis());
      EXPECT_TRUE(seed.has_start_time_millis());
      EXPECT_EQ(expected_seed.start_time_millis(), seed.start_time_millis());

      EXPECT_TRUE(expected_seed.has_end_time_millis());
      EXPECT_TRUE(seed.has_end_time_millis());
      EXPECT_EQ(expected_seed.end_time_millis(), seed.end_time_millis());
    }

    EXPECT_EQ(expected_device.has_arc_plus_plus(), device.has_arc_plus_plus());
    EXPECT_EQ(expected_device.arc_plus_plus(), device.arc_plus_plus());

    EXPECT_EQ(expected_device.has_pixel_phone(), device.has_pixel_phone());
    EXPECT_EQ(expected_device.pixel_phone(), device.pixel_phone());

    EXPECT_EQ(expected_device.supported_software_features_size(),
              device.supported_software_features_size());
    for (const auto& software_feature :
         expected_device.supported_software_features()) {
      EXPECT_TRUE(base::Contains(device.supported_software_features(),
                                 software_feature));
    }

    EXPECT_EQ(expected_device.enabled_software_features_size(),
              device.enabled_software_features_size());
    for (const auto& software_feature :
         expected_device.enabled_software_features()) {
      EXPECT_TRUE(
          base::Contains(device.enabled_software_features(), software_feature));
    }
  }
}

// Validates that |devices| and the corresponding preferences stored by
// |pref_service| are equal to |expected_devices|.
void ExpectSyncedDevicesAndPrefAreEqual(
    const std::vector<cryptauth::ExternalDeviceInfo>& expected_devices,
    const std::vector<cryptauth::ExternalDeviceInfo>& devices,
    const PrefService& pref_service) {
  ExpectSyncedDevicesAreEqual(expected_devices, devices);

  const base::Value::List& synced_devices_pref =
      pref_service.GetList(prefs::kCryptAuthDeviceSyncUnlockKeys);
  ASSERT_EQ(expected_devices.size(), synced_devices_pref.size());
  for (size_t i = 0; i < synced_devices_pref.size(); ++i) {
    SCOPED_TRACE(base::StringPrintf("Compare pref dictionary at index=%d",
                                    static_cast<int>(i)));
    const base::Value::Dict* device_dictionary =
        synced_devices_pref[i].GetIfDict();
    EXPECT_TRUE(device_dictionary);

    const auto& expected_device = expected_devices[i];

    std::string public_key;
    const std::string* public_key_b64 =
        device_dictionary->FindString("public_key");
    EXPECT_TRUE(public_key_b64);
    EXPECT_TRUE(base::Base64UrlDecode(
        *public_key_b64, base::Base64UrlDecodePolicy::REQUIRE_PADDING,
        &public_key));
    EXPECT_TRUE(expected_device.has_public_key());
    EXPECT_EQ(expected_device.public_key(), public_key);

    std::string device_name;
    const std::string* device_name_b64 =
        device_dictionary->FindString("device_name");
    if (device_name_b64) {
      EXPECT_TRUE(base::Base64UrlDecode(
          *device_name_b64, base::Base64UrlDecodePolicy::REQUIRE_PADDING,
          &device_name));
      EXPECT_TRUE(expected_device.has_friendly_device_name());
      EXPECT_EQ(expected_device.friendly_device_name(), device_name);
    } else {
      EXPECT_FALSE(expected_device.has_friendly_device_name());
    }

    std::string no_pii_device_name;
    const std::string* no_pii_device_name_b64 =
        device_dictionary->FindString("no_pii_device_name");
    if (no_pii_device_name_b64) {
      EXPECT_TRUE(base::Base64UrlDecode(
          *no_pii_device_name_b64, base::Base64UrlDecodePolicy::REQUIRE_PADDING,
          &no_pii_device_name));
      EXPECT_TRUE(expected_device.has_no_pii_device_name());
      EXPECT_EQ(expected_device.no_pii_device_name(), no_pii_device_name);
    } else {
      EXPECT_FALSE(expected_device.has_no_pii_device_name());
    }

    std::string bluetooth_address;
    const std::string* bluetooth_address_b64 =
        device_dictionary->FindString("bluetooth_address");
    if (bluetooth_address_b64) {
      EXPECT_TRUE(base::Base64UrlDecode(
          *bluetooth_address_b64, base::Base64UrlDecodePolicy::REQUIRE_PADDING,
          &bluetooth_address));
      EXPECT_TRUE(expected_device.has_bluetooth_address());
      EXPECT_EQ(expected_device.bluetooth_address(), bluetooth_address);
    } else {
      EXPECT_FALSE(expected_device.has_bluetooth_address());
    }

    std::optional<bool> unlock_key = device_dictionary->FindBool("unlock_key");
    if (unlock_key.has_value()) {
      EXPECT_TRUE(expected_device.has_unlock_key());
      EXPECT_EQ(expected_device.unlock_key(), unlock_key.value());
    } else {
      EXPECT_FALSE(expected_device.has_unlock_key());
    }

    std::optional<bool> unlockable = device_dictionary->FindBool("unlockable");
    if (unlockable.has_value()) {
      EXPECT_TRUE(expected_device.has_unlockable());
      EXPECT_EQ(expected_device.unlockable(), unlockable.value());
    } else {
      EXPECT_FALSE(expected_device.has_unlockable());
    }

    const std::string* last_update_time_millis_str =
        device_dictionary->FindString("last_update_time_millis");
    if (last_update_time_millis_str) {
      int64_t last_update_time_millis;
      EXPECT_TRUE(base::StringToInt64(*last_update_time_millis_str,
                                      &last_update_time_millis));
      EXPECT_TRUE(expected_device.has_last_update_time_millis());
      EXPECT_EQ(expected_device.last_update_time_millis(),
                last_update_time_millis);
    } else {
      EXPECT_FALSE(expected_device.has_last_update_time_millis());
    }

    std::optional<bool> mobile_hotspot_supported =
        device_dictionary->FindBool("mobile_hotspot_supported");
    if (mobile_hotspot_supported.has_value()) {
      EXPECT_TRUE(expected_device.has_mobile_hotspot_supported());
      EXPECT_EQ(expected_device.mobile_hotspot_supported(),
                mobile_hotspot_supported.value());
    } else {
      EXPECT_FALSE(expected_device.has_mobile_hotspot_supported());
    }

    std::optional<int> device_type = device_dictionary->FindInt("device_type");
    if (device_type.has_value()) {
      EXPECT_TRUE(expected_device.has_device_type());
      EXPECT_EQ(DeviceTypeStringToEnum(expected_device.device_type()),
                device_type.value());
    } else {
      EXPECT_FALSE(expected_device.has_device_type());
    }
    const base::Value::List* beacon_seeds_from_prefs =
        device_dictionary->FindList("beacon_seeds");
    if (beacon_seeds_from_prefs) {
      ASSERT_EQ(static_cast<size_t>(expected_device.beacon_seeds_size()),
                beacon_seeds_from_prefs->size());
      for (size_t beacon_seed = 0;
           beacon_seed < beacon_seeds_from_prefs->size(); beacon_seed++) {
        const base::Value::Dict& seed =
            (*beacon_seeds_from_prefs)[beacon_seed].GetDict();

        const std::string* data_b64 = seed.FindString("beacon_seed_data");
        EXPECT_TRUE(data_b64);
        const std::string* start_ms = seed.FindString("beacon_seed_start_ms");
        EXPECT_TRUE(start_ms);
        const std::string* end_ms = seed.FindString("beacon_seed_end_ms");
        EXPECT_TRUE(end_ms);

        const cryptauth::BeaconSeed& expected_seed =
            expected_device.beacon_seeds(static_cast<int>(beacon_seed));

        std::string data;
        EXPECT_TRUE(base::Base64UrlDecode(
            *data_b64, base::Base64UrlDecodePolicy::REQUIRE_PADDING, &data));
        EXPECT_TRUE(expected_seed.has_data());
        EXPECT_EQ(expected_seed.data(), data);

        EXPECT_TRUE(expected_seed.has_start_time_millis());
        EXPECT_EQ(base::NumberToString(expected_seed.start_time_millis()),
                  *start_ms);

        EXPECT_TRUE(expected_seed.has_end_time_millis());
        EXPECT_EQ(base::NumberToString(expected_seed.end_time_millis()),
                  *end_ms);
      }
    } else {
      EXPECT_FALSE(expected_device.beacon_seeds_size());
    }

    std::optional<bool> arc_plus_plus =
        device_dictionary->FindBool("arc_plus_plus");
    if (arc_plus_plus.has_value()) {
      EXPECT_TRUE(expected_device.has_arc_plus_plus());
      EXPECT_EQ(expected_device.arc_plus_plus(), arc_plus_plus.value());
    } else {
      EXPECT_FALSE(expected_device.has_arc_plus_plus());
    }

    std::optional<bool> pixel_phone =
        device_dictionary->FindBool("pixel_phone");
    if (pixel_phone.has_value()) {
      EXPECT_TRUE(expected_device.has_pixel_phone());
      EXPECT_EQ(expected_device.pixel_phone(), pixel_phone.value());
    } else {
      EXPECT_FALSE(expected_device.has_pixel_phone());
    }

    const base::Value::Dict* software_features_from_prefs =
        device_dictionary->FindDict("software_features");
    if (software_features_from_prefs) {
      std::vector<cryptauth::SoftwareFeature> supported_software_features;
      std::vector<cryptauth::SoftwareFeature> enabled_software_features;

      for (const auto it : *software_features_from_prefs) {
        ASSERT_TRUE(it.second.is_int());

        cryptauth::SoftwareFeature software_feature =
            SoftwareFeatureStringToEnum(it.first);
        switch (static_cast<multidevice::SoftwareFeatureState>(
            it.second.GetInt())) {
          case multidevice::SoftwareFeatureState::kEnabled:
            enabled_software_features.push_back(software_feature);
            [[fallthrough]];
          case multidevice::SoftwareFeatureState::kSupported:
            supported_software_features.push_back(software_feature);
            break;
          default:
            break;
        }
      }

      ASSERT_EQ(static_cast<size_t>(
                    expected_device.supported_software_features_size()),
                supported_software_features.size());
      ASSERT_EQ(
          static_cast<size_t>(expected_device.enabled_software_features_size()),
          enabled_software_features.size());
      for (auto supported_software_feature :
           expected_device.supported_software_features()) {
        EXPECT_TRUE(base::Contains(
            supported_software_features,
            SoftwareFeatureStringToEnum(supported_software_feature)));
      }
      for (auto enabled_software_feature :
           expected_device.enabled_software_features()) {
        EXPECT_TRUE(base::Contains(
            enabled_software_features,
            SoftwareFeatureStringToEnum(enabled_software_feature)));
      }
    } else {
      EXPECT_FALSE(expected_device.supported_software_features_size());
      EXPECT_FALSE(expected_device.enabled_software_features_size());
    }
  }
}

// Harness for testing CryptAuthDeviceManager.
class TestCryptAuthDeviceManager : public CryptAuthDeviceManagerImpl {
 public:
  TestCryptAuthDeviceManager(base::Clock* clock,
                             CryptAuthClientFactory* client_factory,
                             CryptAuthGCMManager* gcm_manager,
                             PrefService* pref_service)
      : CryptAuthDeviceManagerImpl(clock,
                                   client_factory,
                                   gcm_manager,
                                   pref_service),
        scoped_sync_scheduler_(new NiceMock<MockSyncScheduler>()),
        weak_sync_scheduler_factory_(scoped_sync_scheduler_.get()) {
    SetSyncSchedulerForTest(base::WrapUnique(scoped_sync_scheduler_.get()));
  }

  TestCryptAuthDeviceManager(const TestCryptAuthDeviceManager&) = delete;
  TestCryptAuthDeviceManager& operator=(const TestCryptAuthDeviceManager&) =
      delete;

  ~TestCryptAuthDeviceManager() override {}

  base::WeakPtr<MockSyncScheduler> GetSyncScheduler() {
    return weak_sync_scheduler_factory_.GetWeakPtr();
  }

 private:
  // Ownership is passed to |CryptAuthDeviceManager| super class when
  // SetSyncSchedulerForTest() is called.
  raw_ptr<MockSyncScheduler> scoped_sync_scheduler_;

  // Stores the pointer of |scoped_sync_scheduler_| after ownership is passed to
  // the super class.
  // This should be safe because the life-time this SyncScheduler will always be
  // within the life of the TestCryptAuthDeviceManager object.
  base::WeakPtrFactory<MockSyncScheduler> weak_sync_scheduler_factory_;
};

}  // namespace

class DeviceSyncCryptAuthDeviceManagerImplTest
    : public testing::Test,
      public CryptAuthDeviceManager::Observer,
      public MockCryptAuthClientFactory::Observer {
 public:
  DeviceSyncCryptAuthDeviceManagerImplTest(
      const DeviceSyncCryptAuthDeviceManagerImplTest&) = delete;
  DeviceSyncCryptAuthDeviceManagerImplTest& operator=(
      const DeviceSyncCryptAuthDeviceManagerImplTest&) = delete;

 protected:
  DeviceSyncCryptAuthDeviceManagerImplTest()
      : client_factory_(std::make_unique<MockCryptAuthClientFactory>(
            MockCryptAuthClientFactory::MockType::MAKE_STRICT_MOCKS)),
        gcm_manager_("existing gcm registration id") {
    client_factory_->AddObserver(this);

    cryptauth::ExternalDeviceInfo unlock_key;
    unlock_key.set_public_key(kPublicKey1);
    unlock_key.set_friendly_device_name(kDeviceName1);
    unlock_key.set_no_pii_device_name(kNoPiiDeviceName1);
    unlock_key.set_bluetooth_address(kBluetoothAddress1);
    unlock_key.set_unlockable(kUnlockable1);
    cryptauth::BeaconSeed* seed1 = unlock_key.add_beacon_seeds();
    seed1->set_data(kBeaconSeed1Data);
    seed1->set_start_time_millis(kBeaconSeed1StartTime);
    seed1->set_end_time_millis(kBeaconSeed1EndTime);
    cryptauth::BeaconSeed* seed2 = unlock_key.add_beacon_seeds();
    seed2->set_data(kBeaconSeed2Data);
    seed2->set_start_time_millis(kBeaconSeed2StartTime);
    seed2->set_end_time_millis(kBeaconSeed2EndTime);
    unlock_key.set_arc_plus_plus(kArcPlusPlus1);
    unlock_key.set_pixel_phone(kPixelPhone1);
    unlock_key.add_supported_software_features(SoftwareFeatureEnumToString(
        cryptauth::SoftwareFeature::EASY_UNLOCK_HOST));
    unlock_key.add_enabled_software_features(SoftwareFeatureEnumToString(
        cryptauth::SoftwareFeature::EASY_UNLOCK_HOST));
    unlock_key.add_supported_software_features(SoftwareFeatureEnumToString(
        cryptauth::SoftwareFeature::BETTER_TOGETHER_HOST));
    unlock_key.add_supported_software_features(SoftwareFeatureEnumToString(
        cryptauth::SoftwareFeature::BETTER_TOGETHER_CLIENT));
    unlock_key.add_enabled_software_features(SoftwareFeatureEnumToString(
        cryptauth::SoftwareFeature::BETTER_TOGETHER_HOST));
    devices_in_response_.push_back(unlock_key);

    cryptauth::ExternalDeviceInfo unlockable_device;
    unlockable_device.set_public_key(kPublicKey2);
    unlockable_device.set_friendly_device_name(kDeviceName2);
    unlockable_device.set_no_pii_device_name(kNoPiiDeviceName2);
    unlockable_device.set_unlockable(kUnlockable2);
    cryptauth::BeaconSeed* seed3 = unlockable_device.add_beacon_seeds();
    seed3->set_data(kBeaconSeed3Data);
    seed3->set_start_time_millis(kBeaconSeed3StartTime);
    seed3->set_end_time_millis(kBeaconSeed3EndTime);
    cryptauth::BeaconSeed* seed4 = unlockable_device.add_beacon_seeds();
    seed4->set_data(kBeaconSeed4Data);
    seed4->set_start_time_millis(kBeaconSeed4StartTime);
    seed4->set_end_time_millis(kBeaconSeed4EndTime);
    unlockable_device.set_arc_plus_plus(kArcPlusPlus2);
    unlockable_device.set_pixel_phone(kPixelPhone2);
    unlockable_device.add_supported_software_features(
        SoftwareFeatureEnumToString(
            cryptauth::SoftwareFeature::MAGIC_TETHER_HOST));
    unlockable_device.add_supported_software_features(
        SoftwareFeatureEnumToString(
            cryptauth::SoftwareFeature::MAGIC_TETHER_CLIENT));
    unlockable_device.add_enabled_software_features(SoftwareFeatureEnumToString(
        cryptauth::SoftwareFeature::MAGIC_TETHER_HOST));
    devices_in_response_.push_back(unlockable_device);
  }

  ~DeviceSyncCryptAuthDeviceManagerImplTest() override {
    client_factory_->RemoveObserver(this);
  }

  // testing::Test:
  void SetUp() override {
    clock_.SetNow(
        base::Time::FromSecondsSinceUnixEpoch(kInitialTimeNowSeconds));

    CryptAuthDeviceManager::RegisterPrefs(pref_service_.registry());
    pref_service_.SetUserPref(
        prefs::kCryptAuthDeviceSyncIsRecoveringFromFailure,
        std::make_unique<base::Value>(false));
    pref_service_.SetUserPref(
        prefs::kCryptAuthDeviceSyncLastSyncTimeSeconds,
        std::make_unique<base::Value>(kLastSyncTimeSeconds));
    pref_service_.SetUserPref(
        prefs::kCryptAuthDeviceSyncReason,
        std::make_unique<base::Value>(cryptauth::INVOCATION_REASON_UNKNOWN));

    std::string public_key_b64, device_name_b64, bluetooth_address_b64;
    base::Base64UrlEncode(kStoredPublicKey,
                          base::Base64UrlEncodePolicy::INCLUDE_PADDING,
                          &public_key_b64);
    base::Base64UrlEncode(kStoredDeviceName,
                          base::Base64UrlEncodePolicy::INCLUDE_PADDING,
                          &device_name_b64);
    base::Base64UrlEncode(kStoredBluetoothAddress,
                          base::Base64UrlEncodePolicy::INCLUDE_PADDING,
                          &bluetooth_address_b64);

    {
      ScopedListPrefUpdate update(&pref_service_,
                                  prefs::kCryptAuthDeviceSyncUnlockKeys);
      update->Append(base::Value::Dict()
                         .Set("public_key", public_key_b64)
                         .Set("device_name", device_name_b64)
                         .Set("bluetooth_address", bluetooth_address_b64)
                         .Set("unlockable", kStoredUnlockable)
                         .Set("beacon_seeds", base::Value::List())
                         .Set("software_features", base::Value::Dict()));
    }

    device_manager_ = std::make_unique<TestCryptAuthDeviceManager>(
        &clock_, client_factory_.get(), &gcm_manager_, &pref_service_);
    device_manager_->AddObserver(this);

    get_my_devices_response_.add_devices()->CopyFrom(devices_in_response_[0]);
    get_my_devices_response_.add_devices()->CopyFrom(devices_in_response_[1]);

    ON_CALL(*sync_scheduler(), GetStrategy())
        .WillByDefault(Return(SyncScheduler::Strategy::PERIODIC_REFRESH));
  }

  void TearDown() override { device_manager_->RemoveObserver(this); }

  // CryptAuthDeviceManager::Observer:
  void OnSyncStarted() override { OnSyncStartedProxy(); }

  void OnSyncFinished(CryptAuthDeviceManager::SyncResult sync_result,
                      CryptAuthDeviceManager::DeviceChangeResult
                          device_change_result) override {
    OnSyncFinishedProxy(sync_result, device_change_result);
  }

  MOCK_METHOD0(OnSyncStartedProxy, void());
  MOCK_METHOD2(OnSyncFinishedProxy,
               void(CryptAuthDeviceManager::SyncResult,
                    CryptAuthDeviceManager::DeviceChangeResult));

  // Simulates firing the SyncScheduler to trigger a device sync attempt.
  void FireSchedulerForSync(
      cryptauth::InvocationReason expected_invocation_reason) {
    SyncScheduler::Delegate* delegate =
        static_cast<SyncScheduler::Delegate*>(device_manager_.get());

    std::unique_ptr<SyncScheduler::SyncRequest> sync_request =
        std::make_unique<SyncScheduler::SyncRequest>(
            device_manager_->GetSyncScheduler());
    EXPECT_CALL(*this, OnSyncStartedProxy());
    delegate->OnSyncRequested(std::move(sync_request));

    EXPECT_EQ(expected_invocation_reason,
              get_my_devices_request_.invocation_reason());

    // The allow_stale_read flag is set if the sync was not forced.
    bool allow_stale_read =
        expected_invocation_reason !=
            cryptauth::INVOCATION_REASON_FEATURE_TOGGLED &&
        expected_invocation_reason !=
            cryptauth::INVOCATION_REASON_SERVER_INITIATED &&
        expected_invocation_reason != cryptauth::INVOCATION_REASON_MANUAL;
    EXPECT_EQ(allow_stale_read, get_my_devices_request_.allow_stale_read());
  }

  // MockCryptAuthClientFactory::Observer:
  void OnCryptAuthClientCreated(MockCryptAuthClient* client) override {
    EXPECT_CALL(*client, GetMyDevices_(_, _, _, _))
        .WillOnce(DoAll(SaveArg<0>(&get_my_devices_request_),
                        MoveArg<1>(&success_callback_),
                        MoveArg<2>(&error_callback_)));
  }

  MockSyncScheduler* sync_scheduler() {
    return device_manager_->GetSyncScheduler().get();
  }

  base::SimpleTestClock clock_;

  std::unique_ptr<MockCryptAuthClientFactory> client_factory_;

  TestingPrefServiceSimple pref_service_;

  FakeCryptAuthGCMManager gcm_manager_;

  std::unique_ptr<TestCryptAuthDeviceManager> device_manager_;

  std::vector<cryptauth::ExternalDeviceInfo> devices_in_response_;

  cryptauth::GetMyDevicesResponse get_my_devices_response_;

  cryptauth::GetMyDevicesRequest get_my_devices_request_;

  CryptAuthClient::GetMyDevicesCallback success_callback_;

  CryptAuthClient::ErrorCallback error_callback_;
};

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest, RegisterPrefs) {
  TestingPrefServiceSimple pref_service;
  CryptAuthDeviceManager::RegisterPrefs(pref_service.registry());
  EXPECT_TRUE(pref_service.FindPreference(
      prefs::kCryptAuthDeviceSyncLastSyncTimeSeconds));
  EXPECT_TRUE(pref_service.FindPreference(
      prefs::kCryptAuthDeviceSyncIsRecoveringFromFailure));
  EXPECT_TRUE(pref_service.FindPreference(prefs::kCryptAuthDeviceSyncReason));
  EXPECT_TRUE(
      pref_service.FindPreference(prefs::kCryptAuthDeviceSyncUnlockKeys));
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest, GetSyncState) {
  device_manager_->Start();

  ON_CALL(*sync_scheduler(), GetStrategy())
      .WillByDefault(Return(SyncScheduler::Strategy::PERIODIC_REFRESH));
  EXPECT_FALSE(device_manager_->IsRecoveringFromFailure());

  ON_CALL(*sync_scheduler(), GetStrategy())
      .WillByDefault(Return(SyncScheduler::Strategy::AGGRESSIVE_RECOVERY));
  EXPECT_TRUE(device_manager_->IsRecoveringFromFailure());

  base::TimeDelta time_to_next_sync = base::Minutes(60);
  ON_CALL(*sync_scheduler(), GetTimeToNextSync())
      .WillByDefault(Return(time_to_next_sync));
  EXPECT_EQ(time_to_next_sync, device_manager_->GetTimeToNextAttempt());

  ON_CALL(*sync_scheduler(), GetSyncState())
      .WillByDefault(Return(SyncScheduler::SyncState::SYNC_IN_PROGRESS));
  EXPECT_TRUE(device_manager_->IsSyncInProgress());

  ON_CALL(*sync_scheduler(), GetSyncState())
      .WillByDefault(Return(SyncScheduler::SyncState::WAITING_FOR_REFRESH));
  EXPECT_FALSE(device_manager_->IsSyncInProgress());
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest, InitWithDefaultPrefs) {
  base::SimpleTestClock clock;
  clock.SetNow(base::Time::FromSecondsSinceUnixEpoch(kInitialTimeNowSeconds));
  base::TimeDelta elapsed_time =
      clock.Now() - base::Time::FromSecondsSinceUnixEpoch(0);

  TestingPrefServiceSimple pref_service;
  CryptAuthDeviceManager::RegisterPrefs(pref_service.registry());

  TestCryptAuthDeviceManager device_manager(&clock, client_factory_.get(),
                                            &gcm_manager_, &pref_service);

  EXPECT_CALL(
      *(device_manager.GetSyncScheduler()),
      Start(elapsed_time, SyncScheduler::Strategy::AGGRESSIVE_RECOVERY));
  device_manager.Start();
  EXPECT_TRUE(device_manager.GetLastSyncTime().is_null());
  EXPECT_EQ(0u, device_manager.GetSyncedDevices().size());
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest, InitWithExistingPrefs) {
  EXPECT_CALL(*sync_scheduler(),
              Start(clock_.Now() - base::Time::FromSecondsSinceUnixEpoch(
                                       kLastSyncTimeSeconds),
                    SyncScheduler::Strategy::PERIODIC_REFRESH));

  device_manager_->Start();
  EXPECT_EQ(base::Time::FromSecondsSinceUnixEpoch(kLastSyncTimeSeconds),
            device_manager_->GetLastSyncTime());

  auto synced_devices = device_manager_->GetSyncedDevices();
  ASSERT_EQ(1u, synced_devices.size());
  EXPECT_EQ(kStoredPublicKey, synced_devices[0].public_key());
  EXPECT_EQ(kStoredDeviceName, synced_devices[0].friendly_device_name());
  EXPECT_EQ(kStoredBluetoothAddress, synced_devices[0].bluetooth_address());
  EXPECT_EQ(kStoredUnlockable, synced_devices[0].unlockable());
}

// cryptauth::ExternalDeviceInfos's |unlock_key| and |mobile_hotspot_supported|
// fields are deprecated, but it may be the case that after an update to Chrome,
// the prefs reflect the old style of using these deprecated fields, instead of
// software features. This test ensures the CryptAuthDeviceManager considers
// these deprecated booleans, and populates the correct software features.
TEST_F(
    DeviceSyncCryptAuthDeviceManagerImplTest,
    InitWithExistingPrefs_MigrateDeprecateBooleansFromPrefsToSoftwareFeature) {
  ScopedListPrefUpdate update_clear(&pref_service_,
                                    prefs::kCryptAuthDeviceSyncUnlockKeys);
  update_clear->clear();

  // Simulate a deprecated device being persisted to prefs.
  std::string public_key_b64;
  base::Base64UrlEncode(kStoredPublicKey,
                        base::Base64UrlEncodePolicy::INCLUDE_PADDING,
                        &public_key_b64);

  ScopedListPrefUpdate update(&pref_service_,
                              prefs::kCryptAuthDeviceSyncUnlockKeys);
  update->Append(base::Value::Dict()
                     .Set("public_key", public_key_b64)
                     .Set("unlock_key", true)
                     .Set("mobile_hotspot_supported", true)
                     .Set("software_features", base::Value::Dict()));

  device_manager_ = std::make_unique<TestCryptAuthDeviceManager>(
      &clock_, client_factory_.get(), &gcm_manager_, &pref_service_);
  device_manager_->Start();

  // Ensure that the deprecated booleans are not exposed in the final
  // cryptauth::ExternalDeviceInfo, but rather in the correct software features.
  auto synced_devices = device_manager_->GetSyncedDevices();
  ASSERT_EQ(1u, synced_devices.size());
  EXPECT_EQ(kStoredPublicKey, synced_devices[0].public_key());
  EXPECT_FALSE(synced_devices[0].unlock_key());
  EXPECT_FALSE(synced_devices[0].mobile_hotspot_supported());

  EXPECT_EQ(2, synced_devices[0].supported_software_features().size());
  EXPECT_TRUE(
      base::Contains(synced_devices[0].supported_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::EASY_UNLOCK_HOST)));
  EXPECT_TRUE(
      base::Contains(synced_devices[0].supported_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::MAGIC_TETHER_HOST)));
  EXPECT_EQ(1, synced_devices[0].enabled_software_features().size());
  EXPECT_TRUE(
      base::Contains(synced_devices[0].enabled_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::EASY_UNLOCK_HOST)));
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest, SyncSucceedsForFirstTime) {
  pref_service_.ClearPref(prefs::kCryptAuthDeviceSyncLastSyncTimeSeconds);
  device_manager_->Start();

  FireSchedulerForSync(cryptauth::INVOCATION_REASON_INITIALIZATION);
  ASSERT_FALSE(success_callback_.is_null());

  clock_.SetNow(base::Time::FromSecondsSinceUnixEpoch(kLaterTimeNowSeconds));
  EXPECT_CALL(*this, OnSyncFinishedProxy(
                         CryptAuthDeviceManager::SyncResult::SUCCESS,
                         CryptAuthDeviceManager::DeviceChangeResult::CHANGED));

  std::move(success_callback_).Run(get_my_devices_response_);
  EXPECT_EQ(clock_.Now(), device_manager_->GetLastSyncTime());

  ExpectSyncedDevicesAndPrefAreEqual(
      devices_in_response_, device_manager_->GetSyncedDevices(), pref_service_);
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest, ForceSync) {
  device_manager_->Start();

  EXPECT_CALL(*sync_scheduler(), ForceSync());
  device_manager_->ForceSyncNow(cryptauth::INVOCATION_REASON_MANUAL);

  FireSchedulerForSync(cryptauth::INVOCATION_REASON_MANUAL);

  clock_.SetNow(base::Time::FromSecondsSinceUnixEpoch(kLaterTimeNowSeconds));
  EXPECT_CALL(*this, OnSyncFinishedProxy(
                         CryptAuthDeviceManager::SyncResult::SUCCESS,
                         CryptAuthDeviceManager::DeviceChangeResult::CHANGED));
  std::move(success_callback_).Run(get_my_devices_response_);
  EXPECT_EQ(clock_.Now(), device_manager_->GetLastSyncTime());

  ExpectSyncedDevicesAndPrefAreEqual(
      devices_in_response_, device_manager_->GetSyncedDevices(), pref_service_);
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest, ForceSyncFailsThenSucceeds) {
  device_manager_->Start();
  EXPECT_FALSE(pref_service_.GetBoolean(
      prefs::kCryptAuthDeviceSyncIsRecoveringFromFailure));
  base::Time old_sync_time = device_manager_->GetLastSyncTime();

  // The first force sync fails.
  EXPECT_CALL(*sync_scheduler(), ForceSync());
  device_manager_->ForceSyncNow(cryptauth::INVOCATION_REASON_MANUAL);
  FireSchedulerForSync(cryptauth::INVOCATION_REASON_MANUAL);
  clock_.SetNow(base::Time::FromSecondsSinceUnixEpoch(kLaterTimeNowSeconds));
  EXPECT_CALL(*this,
              OnSyncFinishedProxy(
                  CryptAuthDeviceManager::SyncResult::FAILURE,
                  CryptAuthDeviceManager::DeviceChangeResult::UNCHANGED));
  std::move(error_callback_).Run(NetworkRequestError::kEndpointNotFound);
  EXPECT_EQ(old_sync_time, device_manager_->GetLastSyncTime());
  EXPECT_TRUE(pref_service_.GetBoolean(
      prefs::kCryptAuthDeviceSyncIsRecoveringFromFailure));
  EXPECT_EQ(static_cast<int>(cryptauth::INVOCATION_REASON_MANUAL),
            pref_service_.GetInteger(prefs::kCryptAuthDeviceSyncReason));

  // The second recovery sync succeeds.
  ON_CALL(*sync_scheduler(), GetStrategy())
      .WillByDefault(Return(SyncScheduler::Strategy::AGGRESSIVE_RECOVERY));
  FireSchedulerForSync(cryptauth::INVOCATION_REASON_MANUAL);
  clock_.SetNow(
      base::Time::FromSecondsSinceUnixEpoch(kLaterTimeNowSeconds + 30));
  EXPECT_CALL(*this, OnSyncFinishedProxy(
                         CryptAuthDeviceManager::SyncResult::SUCCESS,
                         CryptAuthDeviceManager::DeviceChangeResult::CHANGED));
  std::move(success_callback_).Run(get_my_devices_response_);
  EXPECT_EQ(clock_.Now(), device_manager_->GetLastSyncTime());

  ExpectSyncedDevicesAndPrefAreEqual(
      devices_in_response_, device_manager_->GetSyncedDevices(), pref_service_);

  EXPECT_FLOAT_EQ(
      clock_.Now().InSecondsFSinceUnixEpoch(),
      pref_service_.GetDouble(prefs::kCryptAuthDeviceSyncLastSyncTimeSeconds));
  EXPECT_EQ(static_cast<int>(cryptauth::INVOCATION_REASON_UNKNOWN),
            pref_service_.GetInteger(prefs::kCryptAuthDeviceSyncReason));
  EXPECT_FALSE(pref_service_.GetBoolean(
      prefs::kCryptAuthDeviceSyncIsRecoveringFromFailure));
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest,
       PeriodicSyncFailsThenSucceeds) {
  device_manager_->Start();
  base::Time old_sync_time = device_manager_->GetLastSyncTime();

  // The first periodic sync fails.
  FireSchedulerForSync(cryptauth::INVOCATION_REASON_PERIODIC);
  clock_.SetNow(base::Time::FromSecondsSinceUnixEpoch(kLaterTimeNowSeconds));
  EXPECT_CALL(*this,
              OnSyncFinishedProxy(
                  CryptAuthDeviceManager::SyncResult::FAILURE,
                  CryptAuthDeviceManager::DeviceChangeResult::UNCHANGED));
  std::move(error_callback_).Run(NetworkRequestError::kAuthenticationError);
  EXPECT_EQ(old_sync_time, device_manager_->GetLastSyncTime());
  EXPECT_TRUE(pref_service_.GetBoolean(
      prefs::kCryptAuthDeviceSyncIsRecoveringFromFailure));

  // The second recovery sync succeeds.
  ON_CALL(*sync_scheduler(), GetStrategy())
      .WillByDefault(Return(SyncScheduler::Strategy::AGGRESSIVE_RECOVERY));
  FireSchedulerForSync(cryptauth::INVOCATION_REASON_FAILURE_RECOVERY);
  clock_.SetNow(
      base::Time::FromSecondsSinceUnixEpoch(kLaterTimeNowSeconds + 30));
  EXPECT_CALL(*this, OnSyncFinishedProxy(
                         CryptAuthDeviceManager::SyncResult::SUCCESS,
                         CryptAuthDeviceManager::DeviceChangeResult::CHANGED));
  std::move(success_callback_).Run(get_my_devices_response_);
  EXPECT_EQ(clock_.Now(), device_manager_->GetLastSyncTime());

  ExpectSyncedDevicesAndPrefAreEqual(
      devices_in_response_, device_manager_->GetSyncedDevices(), pref_service_);

  EXPECT_FLOAT_EQ(
      clock_.Now().InSecondsFSinceUnixEpoch(),
      pref_service_.GetDouble(prefs::kCryptAuthDeviceSyncLastSyncTimeSeconds));
  EXPECT_FALSE(pref_service_.GetBoolean(
      prefs::kCryptAuthDeviceSyncIsRecoveringFromFailure));
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest, SyncSameDevice) {
  device_manager_->Start();
  auto original_devices = device_manager_->GetSyncedDevices();

  // Sync new devices.
  FireSchedulerForSync(cryptauth::INVOCATION_REASON_PERIODIC);
  ASSERT_FALSE(success_callback_.is_null());
  EXPECT_CALL(*this,
              OnSyncFinishedProxy(
                  CryptAuthDeviceManager::SyncResult::SUCCESS,
                  CryptAuthDeviceManager::DeviceChangeResult::UNCHANGED));

  // Sync the same device.
  cryptauth::ExternalDeviceInfo synced_device;
  synced_device.set_public_key(kStoredPublicKey);
  synced_device.set_friendly_device_name(kStoredDeviceName);
  synced_device.set_bluetooth_address(kStoredBluetoothAddress);
  synced_device.set_unlockable(kStoredUnlockable);
  cryptauth::GetMyDevicesResponse get_my_devices_response;
  get_my_devices_response.add_devices()->CopyFrom(synced_device);
  std::move(success_callback_).Run(get_my_devices_response);

  // Check that devices are still the same after sync.
  ExpectSyncedDevicesAndPrefAreEqual(
      original_devices, device_manager_->GetSyncedDevices(), pref_service_);
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest, SyncEmptyDeviceList) {
  cryptauth::GetMyDevicesResponse empty_response;

  device_manager_->Start();
  EXPECT_EQ(1u, device_manager_->GetSyncedDevices().size());

  FireSchedulerForSync(cryptauth::INVOCATION_REASON_PERIODIC);
  ASSERT_FALSE(success_callback_.is_null());
  EXPECT_CALL(*this, OnSyncFinishedProxy(
                         CryptAuthDeviceManager::SyncResult::SUCCESS,
                         CryptAuthDeviceManager::DeviceChangeResult::CHANGED));
  std::move(success_callback_).Run(empty_response);

  ExpectSyncedDevicesAndPrefAreEqual(
      std::vector<cryptauth::ExternalDeviceInfo>(),
      device_manager_->GetSyncedDevices(), pref_service_);
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest, SyncThreeDevices) {
  cryptauth::GetMyDevicesResponse response(get_my_devices_response_);
  cryptauth::ExternalDeviceInfo synced_device2;
  synced_device2.set_public_key("new public key");
  synced_device2.set_friendly_device_name("new device name");
  synced_device2.set_bluetooth_address("aa:bb:cc:dd:ee:ff");
  synced_device2.add_supported_software_features(SoftwareFeatureEnumToString(
      cryptauth::SoftwareFeature::EASY_UNLOCK_HOST));
  synced_device2.add_enabled_software_features(SoftwareFeatureEnumToString(
      cryptauth::SoftwareFeature::EASY_UNLOCK_HOST));

  response.add_devices()->CopyFrom(synced_device2);

  std::vector<cryptauth::ExternalDeviceInfo> expected_devices;
  expected_devices.push_back(devices_in_response_[0]);
  expected_devices.push_back(devices_in_response_[1]);
  expected_devices.push_back(synced_device2);

  device_manager_->Start();
  EXPECT_EQ(1u, device_manager_->GetSyncedDevices().size());
  EXPECT_EQ(
      1u, pref_service_.GetList(prefs::kCryptAuthDeviceSyncUnlockKeys).size());

  FireSchedulerForSync(cryptauth::INVOCATION_REASON_PERIODIC);
  ASSERT_FALSE(success_callback_.is_null());
  EXPECT_CALL(*this, OnSyncFinishedProxy(
                         CryptAuthDeviceManager::SyncResult::SUCCESS,
                         CryptAuthDeviceManager::DeviceChangeResult::CHANGED));
  std::move(success_callback_).Run(response);

  ExpectSyncedDevicesAndPrefAreEqual(
      expected_devices, device_manager_->GetSyncedDevices(), pref_service_);
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest, SyncOnGCMPushMessage) {
  device_manager_->Start();

  EXPECT_CALL(*sync_scheduler(), ForceSync());
  gcm_manager_.PushResyncMessage(std::nullopt /* session_id */,
                                 std::nullopt /* feature_type */);

  FireSchedulerForSync(cryptauth::INVOCATION_REASON_SERVER_INITIATED);

  EXPECT_CALL(*this, OnSyncFinishedProxy(
                         CryptAuthDeviceManager::SyncResult::SUCCESS,
                         CryptAuthDeviceManager::DeviceChangeResult::CHANGED));
  std::move(success_callback_).Run(get_my_devices_response_);

  ExpectSyncedDevicesAndPrefAreEqual(
      devices_in_response_, device_manager_->GetSyncedDevices(), pref_service_);
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest, SyncDeviceWithNoContents) {
  device_manager_->Start();

  EXPECT_CALL(*sync_scheduler(), ForceSync());
  gcm_manager_.PushResyncMessage(std::nullopt /* session_id */,
                                 std::nullopt /* feature_type */);

  FireSchedulerForSync(cryptauth::INVOCATION_REASON_SERVER_INITIATED);

  EXPECT_CALL(*this, OnSyncFinishedProxy(
                         CryptAuthDeviceManager::SyncResult::SUCCESS,
                         CryptAuthDeviceManager::DeviceChangeResult::CHANGED));
  std::move(success_callback_).Run(get_my_devices_response_);

  ExpectSyncedDevicesAndPrefAreEqual(
      devices_in_response_, device_manager_->GetSyncedDevices(), pref_service_);
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest,
       SyncFullyDetailedExternalDeviceInfos) {
  // First, use a device with only a public key (a public key is the only
  // required field). This ensures devices work properly when they do not have
  // all fields filled out.
  cryptauth::ExternalDeviceInfo device_with_only_public_key;
  device_with_only_public_key.set_public_key("publicKey1");
  device_with_only_public_key.add_supported_software_features(
      SoftwareFeatureEnumToString(
          cryptauth::SoftwareFeature::EASY_UNLOCK_HOST));
  device_with_only_public_key.add_enabled_software_features(
      SoftwareFeatureEnumToString(
          cryptauth::SoftwareFeature::EASY_UNLOCK_HOST));

  // Second, use a device with all fields filled out. This ensures that all
  // device details are properly saved.
  cryptauth::ExternalDeviceInfo device_with_all_fields;
  device_with_all_fields.set_public_key("publicKey2");
  device_with_all_fields.set_friendly_device_name("deviceName");
  device_with_all_fields.set_bluetooth_address("aa:bb:cc:dd:ee:ff");
  device_with_all_fields.set_unlockable(true);
  device_with_all_fields.set_last_update_time_millis(123456789L);
  device_with_all_fields.set_device_type(
      DeviceTypeEnumToString(cryptauth::DeviceType::ANDROID));

  cryptauth::BeaconSeed seed1;
  seed1.set_data(kBeaconSeed1Data);
  seed1.set_start_time_millis(kBeaconSeed1StartTime);
  seed1.set_end_time_millis(kBeaconSeed1EndTime);
  device_with_all_fields.add_beacon_seeds()->CopyFrom(seed1);

  cryptauth::BeaconSeed seed2;
  seed2.set_data(kBeaconSeed2Data);
  seed2.set_start_time_millis(kBeaconSeed2StartTime);
  seed2.set_end_time_millis(kBeaconSeed2EndTime);
  device_with_all_fields.add_beacon_seeds()->CopyFrom(seed2);

  device_with_all_fields.set_arc_plus_plus(true);
  device_with_all_fields.set_pixel_phone(true);

  device_with_all_fields.add_supported_software_features(
      SoftwareFeatureEnumToString(
          cryptauth::SoftwareFeature::EASY_UNLOCK_HOST));
  device_with_all_fields.add_supported_software_features(
      SoftwareFeatureEnumToString(
          cryptauth::SoftwareFeature::MAGIC_TETHER_HOST));

  device_with_all_fields.add_enabled_software_features(
      SoftwareFeatureEnumToString(
          cryptauth::SoftwareFeature::EASY_UNLOCK_HOST));

  std::vector<cryptauth::ExternalDeviceInfo> expected_devices;
  expected_devices.push_back(device_with_only_public_key);
  expected_devices.push_back(device_with_all_fields);

  device_manager_->Start();
  FireSchedulerForSync(cryptauth::INVOCATION_REASON_PERIODIC);
  ASSERT_FALSE(success_callback_.is_null());
  EXPECT_CALL(*this, OnSyncFinishedProxy(
                         CryptAuthDeviceManager::SyncResult::SUCCESS,
                         CryptAuthDeviceManager::DeviceChangeResult::CHANGED));

  cryptauth::GetMyDevicesResponse response;
  response.add_devices()->CopyFrom(device_with_only_public_key);
  response.add_devices()->CopyFrom(device_with_all_fields);
  std::move(success_callback_).Run(response);

  ExpectSyncedDevicesAndPrefAreEqual(
      expected_devices, device_manager_->GetSyncedDevices(), pref_service_);
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest, SubsetsOfSyncedDevices) {
  device_manager_->Start();

  FireSchedulerForSync(cryptauth::INVOCATION_REASON_PERIODIC);
  ASSERT_FALSE(success_callback_.is_null());
  EXPECT_CALL(*this, OnSyncFinishedProxy(
                         CryptAuthDeviceManager::SyncResult::SUCCESS,
                         CryptAuthDeviceManager::DeviceChangeResult::CHANGED));
  std::move(success_callback_).Run(get_my_devices_response_);

  // All synced devices.
  ExpectSyncedDevicesAndPrefAreEqual(
      devices_in_response_, device_manager_->GetSyncedDevices(), pref_service_);

  // Only unlock keys.
  ExpectSyncedDevicesAreEqual(
      std::vector<cryptauth::ExternalDeviceInfo>(1, devices_in_response_[0]),
      device_manager_->GetUnlockKeys());

  // Only tether hosts.
  ExpectSyncedDevicesAreEqual(
      std::vector<cryptauth::ExternalDeviceInfo>(1, devices_in_response_[1]),
      device_manager_->GetTetherHosts());
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest,
       TestDeprecatedBooleansArePersistedOnlyAsSoftwareFeatures) {
  device_manager_->Start();

  cryptauth::ExternalDeviceInfo device;
  device.set_public_key("public key");
  device.set_friendly_device_name("deprecated device");
  device.set_unlock_key(true);
  device.set_mobile_hotspot_supported(true);

  devices_in_response_.push_back(device);
  get_my_devices_response_.add_devices()->CopyFrom(device);

  FireSchedulerForSync(cryptauth::INVOCATION_REASON_PERIODIC);
  ASSERT_FALSE(success_callback_.is_null());
  EXPECT_CALL(*this, OnSyncFinishedProxy(
                         CryptAuthDeviceManager::SyncResult::SUCCESS,
                         CryptAuthDeviceManager::DeviceChangeResult::CHANGED));
  std::move(success_callback_).Run(get_my_devices_response_);

  cryptauth::ExternalDeviceInfo synced_device =
      device_manager_->GetSyncedDevices()[2];

  EXPECT_FALSE(synced_device.unlock_key());
  EXPECT_FALSE(synced_device.mobile_hotspot_supported());

  EXPECT_TRUE(
      base::Contains(synced_device.supported_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::EASY_UNLOCK_HOST)));
  EXPECT_TRUE(
      base::Contains(synced_device.enabled_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::EASY_UNLOCK_HOST)));
  EXPECT_TRUE(
      base::Contains(synced_device.supported_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::MAGIC_TETHER_HOST)));
  EXPECT_FALSE(
      base::Contains(synced_device.enabled_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::MAGIC_TETHER_HOST)));
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest,
       TestIgnoreDeprecatedBooleansIfSoftwareFeaturesArePresent) {
  device_manager_->Start();

  cryptauth::ExternalDeviceInfo device;
  device.set_public_key("public key");
  device.set_friendly_device_name("deprecated device");
  device.set_unlock_key(false);
  device.set_mobile_hotspot_supported(false);

  device.add_supported_software_features(SoftwareFeatureEnumToString(
      cryptauth::SoftwareFeature::EASY_UNLOCK_HOST));
  device.add_enabled_software_features(SoftwareFeatureEnumToString(
      cryptauth::SoftwareFeature::EASY_UNLOCK_HOST));
  device.add_supported_software_features(SoftwareFeatureEnumToString(
      cryptauth::SoftwareFeature::MAGIC_TETHER_HOST));

  devices_in_response_.push_back(device);
  get_my_devices_response_.add_devices()->CopyFrom(device);

  FireSchedulerForSync(cryptauth::INVOCATION_REASON_PERIODIC);
  ASSERT_FALSE(success_callback_.is_null());
  EXPECT_CALL(*this, OnSyncFinishedProxy(
                         CryptAuthDeviceManager::SyncResult::SUCCESS,
                         CryptAuthDeviceManager::DeviceChangeResult::CHANGED));
  std::move(success_callback_).Run(get_my_devices_response_);

  cryptauth::ExternalDeviceInfo synced_device =
      device_manager_->GetSyncedDevices()[2];

  EXPECT_FALSE(synced_device.unlock_key());
  EXPECT_FALSE(synced_device.mobile_hotspot_supported());

  EXPECT_TRUE(
      base::Contains(synced_device.supported_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::EASY_UNLOCK_HOST)));
  EXPECT_TRUE(
      base::Contains(synced_device.enabled_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::EASY_UNLOCK_HOST)));
  EXPECT_TRUE(
      base::Contains(synced_device.supported_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::MAGIC_TETHER_HOST)));
  EXPECT_FALSE(
      base::Contains(synced_device.enabled_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::MAGIC_TETHER_HOST)));
}

// Regression test for crbug.com/888031.
TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest,
       TestMigrateFromIntToStringSoftwareFeaturePrefRepresentation) {
  device_manager_->Start();

  cryptauth::ExternalDeviceInfo device;
  device.set_public_key("public key");
  device.set_friendly_device_name("deprecated device");

  // Simulate how older client versions persisted SoftwareFeatures as ints.
  device.add_supported_software_features(
      base::NumberToString(cryptauth::SoftwareFeature::EASY_UNLOCK_HOST));
  device.add_enabled_software_features(
      base::NumberToString(cryptauth::SoftwareFeature::EASY_UNLOCK_HOST));
  device.add_supported_software_features(
      base::NumberToString(cryptauth::SoftwareFeature::MAGIC_TETHER_HOST));

  devices_in_response_.push_back(device);
  get_my_devices_response_.add_devices()->CopyFrom(device);

  FireSchedulerForSync(cryptauth::INVOCATION_REASON_PERIODIC);
  ASSERT_FALSE(success_callback_.is_null());
  EXPECT_CALL(*this, OnSyncFinishedProxy(
                         CryptAuthDeviceManager::SyncResult::SUCCESS,
                         CryptAuthDeviceManager::DeviceChangeResult::CHANGED));
  std::move(success_callback_).Run(get_my_devices_response_);

  cryptauth::ExternalDeviceInfo synced_device =
      device_manager_->GetSyncedDevices()[2];

  // CryptAuthDeviceManager should recognize that the SoftwareFeature prefs had
  // been stored as refs, and convert them to their full string representations.
  EXPECT_TRUE(
      base::Contains(synced_device.supported_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::EASY_UNLOCK_HOST)));
  EXPECT_TRUE(
      base::Contains(synced_device.enabled_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::EASY_UNLOCK_HOST)));
  EXPECT_TRUE(
      base::Contains(synced_device.supported_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::MAGIC_TETHER_HOST)));
  EXPECT_FALSE(
      base::Contains(synced_device.enabled_software_features(),
                     SoftwareFeatureEnumToString(
                         cryptauth::SoftwareFeature::MAGIC_TETHER_HOST)));
}

TEST_F(DeviceSyncCryptAuthDeviceManagerImplTest,
       MetricsForEnabledAndNotSupported) {
  cryptauth::ExternalDeviceInfo enabled_not_supported_device;
  enabled_not_supported_device.set_public_key("new public key");
  enabled_not_supported_device.add_supported_software_features(
      SoftwareFeatureEnumToString(
          cryptauth::SoftwareFeature::BETTER_TOGETHER_HOST));
  enabled_not_supported_device.add_enabled_software_features(
      SoftwareFeatureEnumToString(
          cryptauth::SoftwareFeature::BETTER_TOGETHER_HOST));

  // EASY_UNLOCK_HOST is a special case; it is allowed to not be marked as
  // supported, but still be set as enabled.
  enabled_not_supported_device.add_enabled_software_features(
      SoftwareFeatureEnumToString(
          cryptauth::SoftwareFeature::EASY_UNLOCK_HOST));

  // These will fail because they are not set as supported.
  enabled_not_supported_device.add_enabled_software_features(
      SoftwareFeatureEnumToString(
          cryptauth::SoftwareFeature::MAGIC_TETHER_HOST));
  enabled_not_supported_device.add_enabled_software_features(
      "MyUnknownFeature");

  cryptauth::GetMyDevicesResponse response;
  response.add_devices()->CopyFrom(enabled_not_supported_device);

  device_manager_->Start();
  base::HistogramTester histogram_tester;
  histogram_tester.ExpectTotalCount(
      "CryptAuth.DeviceSyncSoftwareFeaturesResult", 0);
  histogram_tester.ExpectTotalCount(
      "CryptAuth.DeviceSyncSoftwareFeaturesResult.Failures", 0);

  EXPECT_EQ(1u, device_manager_->GetSyncedDevices().size());
  FireSchedulerForSync(cryptauth::INVOCATION_REASON_PERIODIC);
  ASSERT_FALSE(success_callback_.is_null());
  EXPECT_CALL(*this, OnSyncFinishedProxy(
                         CryptAuthDeviceManager::SyncResult::SUCCESS,
                         CryptAuthDeviceManager::DeviceChangeResult::CHANGED));
  std::move(success_callback_).Run(response);

  histogram_tester.ExpectTotalCount(
      "CryptAuth.DeviceSyncSoftwareFeaturesResult", 4);
  histogram_tester.ExpectBucketCount<bool>(
      "CryptAuth.DeviceSyncSoftwareFeaturesResult", true, 2);
  histogram_tester.ExpectBucketCount<bool>(
      "CryptAuth.DeviceSyncSoftwareFeaturesResult", false, 2);

  histogram_tester.ExpectTotalCount(
      "CryptAuth.DeviceSyncSoftwareFeaturesResult.Failures", 2);
  histogram_tester.ExpectBucketCount<cryptauth::SoftwareFeature>(
      "CryptAuth.DeviceSyncSoftwareFeaturesResult.Failures",
      cryptauth::SoftwareFeature::BETTER_TOGETHER_HOST, 0);
  histogram_tester.ExpectBucketCount<cryptauth::SoftwareFeature>(
      "CryptAuth.DeviceSyncSoftwareFeaturesResult.Failures",
      cryptauth::SoftwareFeature::EASY_UNLOCK_HOST, 0);
  histogram_tester.ExpectBucketCount<cryptauth::SoftwareFeature>(
      "CryptAuth.DeviceSyncSoftwareFeaturesResult.Failures",
      cryptauth::SoftwareFeature::MAGIC_TETHER_HOST, 1);
  histogram_tester.ExpectBucketCount<cryptauth::SoftwareFeature>(
      "CryptAuth.DeviceSyncSoftwareFeaturesResult.Failures",
      cryptauth::SoftwareFeature::UNKNOWN_FEATURE, 1);
}

}  // namespace device_sync

}  // namespace ash