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

// Copyright 2019 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_v2_enrollment_manager_impl.h"

#include <memory>
#include <optional>
#include <unordered_map>
#include <utility>

#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/simple_test_clock.h"
#include "base/timer/mock_timer.h"
#include "chromeos/ash/services/device_sync/cryptauth_enrollment_constants.h"
#include "chromeos/ash/services/device_sync/cryptauth_enrollment_result.h"
#include "chromeos/ash/services/device_sync/cryptauth_key_bundle.h"
#include "chromeos/ash/services/device_sync/cryptauth_key_registry.h"
#include "chromeos/ash/services/device_sync/cryptauth_key_registry_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_scheduler.h"
#include "chromeos/ash/services/device_sync/cryptauth_v2_enroller.h"
#include "chromeos/ash/services/device_sync/cryptauth_v2_enroller_impl.h"
#include "chromeos/ash/services/device_sync/fake_cryptauth_gcm_manager.h"
#include "chromeos/ash/services/device_sync/fake_cryptauth_scheduler.h"
#include "chromeos/ash/services/device_sync/fake_cryptauth_v2_enroller.h"
#include "chromeos/ash/services/device_sync/mock_cryptauth_client.h"
#include "chromeos/ash/services/device_sync/pref_names.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_api.pb.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_better_together_feature_metadata.pb.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_client_app_metadata.pb.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_common.pb.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_directive.pb.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_v2_test_util.h"
#include "chromeos/ash/services/device_sync/public/cpp/gcm_constants.h"
#include "chromeos/ash/services/device_sync/value_string_encoding.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/testing_pref_service.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {

namespace device_sync {

namespace {

const char kFakeV1PublicKey[] = "public_key_v1";
const char kFakeV1PrivateKey[] = "private_key_v1";
const char kFakeV2PublicKey[] = "public_key_v2";
const char kFakeV2PrivateKey[] = "private_key_v2";
const char kFakeSessionId[] = "session_id";

constexpr base::TimeDelta kFakeRefreshPeriod = base::Milliseconds(2592000000);
constexpr base::TimeDelta kFakeRetryPeriod = base::Milliseconds(43200000);

const cryptauthv2::PolicyReference& GetClientDirectivePolicyReferenceForTest() {
  static const base::NoDestructor<cryptauthv2::PolicyReference>
      policy_reference([] {
        return cryptauthv2::BuildPolicyReference("policy_reference_name",
                                                 1 /* version */);
      }());

  return *policy_reference;
}

class FakeCryptAuthV2EnrollerFactory : public CryptAuthV2EnrollerImpl::Factory {
 public:
  FakeCryptAuthV2EnrollerFactory(
      const CryptAuthKeyRegistry* expected_key_registry,
      const CryptAuthClientFactory* expected_client_factory)
      : expected_key_registry_(expected_key_registry),
        expected_client_factory_(expected_client_factory) {}

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

  ~FakeCryptAuthV2EnrollerFactory() override = default;

  const std::vector<raw_ptr<FakeCryptAuthV2Enroller, VectorExperimental>>&
  created_instances() {
    return created_instances_;
  }

 private:
  // CryptAuthV2EnrollerImpl::Factory:
  std::unique_ptr<CryptAuthV2Enroller> CreateInstance(
      CryptAuthKeyRegistry* key_registry,
      CryptAuthClientFactory* client_factory,
      std::unique_ptr<base::OneShotTimer> timer =
          std::make_unique<base::MockOneShotTimer>()) override {
    EXPECT_EQ(expected_key_registry_, key_registry);
    EXPECT_EQ(expected_client_factory_, client_factory);

    auto instance = std::make_unique<FakeCryptAuthV2Enroller>();
    created_instances_.push_back(instance.get());

    return instance;
  }

  raw_ptr<const CryptAuthKeyRegistry> expected_key_registry_;
  raw_ptr<const CryptAuthClientFactory> expected_client_factory_;

  std::vector<raw_ptr<FakeCryptAuthV2Enroller, VectorExperimental>>
      created_instances_;
};

}  // namespace

class DeviceSyncCryptAuthV2EnrollmentManagerImplTest
    : public testing::Test,
      CryptAuthEnrollmentManager::Observer {
 protected:
  DeviceSyncCryptAuthV2EnrollmentManagerImplTest()
      : fake_gcm_manager_(cryptauthv2::kTestGcmRegistrationId),
        mock_client_factory_(
            MockCryptAuthClientFactory::MockType::MAKE_NICE_MOCKS) {}

  // testing::Test:
  void SetUp() override {
    test_clock_.SetNow(base::Time::UnixEpoch());

    CryptAuthV2EnrollmentManagerImpl::RegisterPrefs(
        test_pref_service_.registry());
    CryptAuthKeyRegistryImpl::RegisterPrefs(test_pref_service_.registry());

    key_registry_ =
        CryptAuthKeyRegistryImpl::Factory::Create(&test_pref_service_);

    fake_enroller_factory_ = std::make_unique<FakeCryptAuthV2EnrollerFactory>(
        key_registry_.get(), &mock_client_factory_);
    CryptAuthV2EnrollerImpl::Factory::SetFactoryForTesting(
        fake_enroller_factory_.get());

    // Fix the scheduler's ClientDirective PolicyReference to test that it is
    // passed to the enroller properly.
    fake_enrollment_scheduler_.set_client_directive_policy_reference(
        GetClientDirectivePolicyReferenceForTest());
  }

  // testing::Test:
  void TearDown() override {
    if (enrollment_manager_)
      enrollment_manager_->RemoveObserver(this);

    CryptAuthV2EnrollerImpl::Factory::SetFactoryForTesting(nullptr);
  }

  // CryptAuthEnrollmentManager::Observer:
  void OnEnrollmentStarted() override {
    ++num_enrollment_started_notifications_;
  }
  void OnEnrollmentFinished(bool success) override {
    observer_enrollment_finished_success_list_.push_back(success);
  }

  void AddV1UserKeyPairToV1Prefs(const std::string& public_key,
                                 const std::string& private_key) {
    test_pref_service_.Set(prefs::kCryptAuthEnrollmentUserPublicKey,
                           util::EncodeAsValueString(public_key));
    test_pref_service_.Set(prefs::kCryptAuthEnrollmentUserPrivateKey,
                           util::EncodeAsValueString(private_key));
  }

  void CreateEnrollmentManager(
      const cryptauthv2::ClientAppMetadata& client_app_metadata) {
    VerifyUserKeyPairStateHistogram(num_manager_creations_ /* total_count */);

    enrollment_manager_ = CryptAuthV2EnrollmentManagerImpl::Factory::Create(
        client_app_metadata, key_registry_.get(), &mock_client_factory_,
        &fake_gcm_manager_, &fake_enrollment_scheduler_, &test_pref_service_,
        &test_clock_);
    ++num_manager_creations_;

    VerifyUserKeyPairStateHistogram(num_manager_creations_ /* total_count */);

    enrollment_manager_->AddObserver(this);
  }

  void DestroyEnrollmentManager() { enrollment_manager_.reset(); }

  void RequestEnrollmentThroughGcm(
      const std::optional<std::string>& session_id) {
    fake_gcm_manager_.PushReenrollMessage(session_id,
                                          std::nullopt /* feature_type */);
  }

  void VerifyEnrollmentManagerObserversNotifiedOfStart(
      size_t expected_num_enrollment_started_notifications) {
    EXPECT_EQ(expected_num_enrollment_started_notifications,
              num_enrollment_started_notifications_);
  }

  void FinishEnrollmentAttempt(
      size_t expected_enroller_instance_index,
      const cryptauthv2::ClientMetadata& expected_client_metadata,
      const cryptauthv2::ClientAppMetadata& expected_client_app_metadata,
      const CryptAuthEnrollmentResult& enrollment_result) {
    EXPECT_TRUE(enrollment_manager_->IsEnrollmentInProgress());

    // Only the most recently created enroller is valid.
    EXPECT_EQ(fake_enroller_factory_->created_instances().size() - 1,
              expected_enroller_instance_index);
    FakeCryptAuthV2Enroller* enroller =
        fake_enroller_factory_
            ->created_instances()[expected_enroller_instance_index];

    VerifyEnrollerData(enroller, expected_client_metadata,
                       expected_client_app_metadata);

    enroller->FinishAttempt(enrollment_result);

    EXPECT_FALSE(enrollment_manager_->IsEnrollmentInProgress());
  }

  void VerifyEnrollmentResults(
      const std::vector<CryptAuthEnrollmentResult>& expected_results) {
    VerifyResultsSentToEnrollmentManagerObservers(expected_results);
    VerifyResultsSentToScheduler(expected_results);
    VerifyEnrollmentResultHistograms(expected_results);
  }

  void VerifyInvocationReasonHistogram(
      const std::vector<cryptauthv2::ClientMetadata::InvocationReason>&
          expected_invocation_reasons) {
    std::unordered_map<cryptauthv2::ClientMetadata::InvocationReason, size_t>
        reason_to_count_map;
    for (const auto& reason : expected_invocation_reasons)
      ++reason_to_count_map[reason];

    size_t total_count = 0;
    for (const auto& reason_count_pair : reason_to_count_map) {
      histogram_tester_.ExpectBucketCount(
          "CryptAuth.EnrollmentV2.InvocationReason", reason_count_pair.first,
          reason_count_pair.second);
      total_count += reason_count_pair.second;
    }

    histogram_tester_.ExpectTotalCount(
        "CryptAuth.EnrollmentV2.InvocationReason", total_count);
  }

  void VerifyUserKeyPairStateHistogram(size_t total_count) {
    histogram_tester_.ExpectTotalCount(
        "CryptAuth.EnrollmentV2.UserKeyPairState", total_count);
  }

  CryptAuthKeyRegistry* key_registry() { return key_registry_.get(); }

  FakeCryptAuthScheduler* fake_enrollment_scheduler() {
    return &fake_enrollment_scheduler_;
  }

  base::SimpleTestClock* test_clock() { return &test_clock_; }

  const base::HistogramTester* histogram_tester() { return &histogram_tester_; }

  CryptAuthEnrollmentManager* enrollment_manager() {
    return enrollment_manager_.get();
  }

 private:
  void VerifyEnrollerData(
      FakeCryptAuthV2Enroller* enroller,
      const cryptauthv2::ClientMetadata& expected_client_metadata,
      const cryptauthv2::ClientAppMetadata& expected_client_app_metadata) {
    EXPECT_TRUE(enroller->was_enroll_called());
    EXPECT_EQ(expected_client_metadata.SerializeAsString(),
              enroller->client_metadata()->SerializeAsString());
    EXPECT_EQ(expected_client_app_metadata.SerializeAsString(),
              enroller->client_app_metadata()->SerializeAsString());
    EXPECT_EQ(
        GetClientDirectivePolicyReferenceForTest().SerializeAsString(),
        (*enroller->client_directive_policy_reference())->SerializeAsString());
  }

  void VerifyResultsSentToEnrollmentManagerObservers(
      const std::vector<CryptAuthEnrollmentResult>
          expected_enrollment_results) {
    ASSERT_EQ(expected_enrollment_results.size(),
              observer_enrollment_finished_success_list_.size());

    for (size_t i = 0; i < expected_enrollment_results.size(); ++i) {
      EXPECT_EQ(expected_enrollment_results[i].IsSuccess(),
                observer_enrollment_finished_success_list_[i]);
    }
  }

  void VerifyResultsSentToScheduler(const std::vector<CryptAuthEnrollmentResult>
                                        expected_enrollment_results) {
    EXPECT_EQ(expected_enrollment_results,
              fake_enrollment_scheduler()->handled_enrollment_results());
  }

  void VerifyEnrollmentResultHistograms(
      const std::vector<CryptAuthEnrollmentResult>
          expected_enrollment_results) {
    std::unordered_map<CryptAuthEnrollmentResult::ResultCode, size_t>
        result_code_to_count_map;
    for (const auto& result : expected_enrollment_results)
      ++result_code_to_count_map[result.result_code()];

    size_t success_count = 0;
    size_t failure_count = 0;
    for (const auto& result_count_pair : result_code_to_count_map) {
      histogram_tester_.ExpectBucketCount(
          "CryptAuth.EnrollmentV2.Result.ResultCode", result_count_pair.first,
          result_count_pair.second);

      if (CryptAuthEnrollmentResult(result_count_pair.first, std::nullopt)
              .IsSuccess()) {
        success_count += result_count_pair.second;
      } else {
        failure_count += result_count_pair.second;
      }
    }

    histogram_tester_.ExpectTotalCount(
        "CryptAuth.EnrollmentV2.Result.ResultCode",
        success_count + failure_count);
    histogram_tester_.ExpectBucketCount("CryptAuth.EnrollmentV2.Result.Success",
                                        true, success_count);
    histogram_tester_.ExpectBucketCount("CryptAuth.EnrollmentV2.Result.Success",
                                        false, failure_count);
  }

  size_t num_manager_creations_ = 0;
  size_t num_enrollment_started_notifications_ = 0;
  std::vector<bool> observer_enrollment_finished_success_list_;

  TestingPrefServiceSimple test_pref_service_;
  FakeCryptAuthGCMManager fake_gcm_manager_;
  FakeCryptAuthScheduler fake_enrollment_scheduler_;
  base::SimpleTestClock test_clock_;
  MockCryptAuthClientFactory mock_client_factory_;
  base::HistogramTester histogram_tester_;
  std::unique_ptr<CryptAuthKeyRegistry> key_registry_;
  std::unique_ptr<FakeCryptAuthV2EnrollerFactory> fake_enroller_factory_;

  std::unique_ptr<CryptAuthEnrollmentManager> enrollment_manager_;
};

TEST_F(DeviceSyncCryptAuthV2EnrollmentManagerImplTest,
       EnrollmentRequestedFromScheduler_NeverPreviouslyEnrolled) {
  CreateEnrollmentManager(cryptauthv2::GetClientAppMetadataForTest());
  EXPECT_FALSE(fake_enrollment_scheduler()->HasEnrollmentSchedulingStarted());

  enrollment_manager()->Start();
  EXPECT_TRUE(fake_enrollment_scheduler()->HasEnrollmentSchedulingStarted());

  // The user has never enrolled with v1 or v2.
  fake_enrollment_scheduler()->set_last_successful_enrollment_time(
      base::Time());
  EXPECT_TRUE(key_registry()->key_bundles().empty());
  EXPECT_TRUE(enrollment_manager()->GetLastEnrollmentTime().is_null());
  EXPECT_FALSE(enrollment_manager()->IsEnrollmentValid());

  // Scheduler triggers an initialization enrollment request.
  fake_enrollment_scheduler()->set_num_consecutive_enrollment_failures(0);
  fake_enrollment_scheduler()->RequestEnrollment(
      cryptauthv2::ClientMetadata::INITIALIZATION /* invocation_reason */,
      std::nullopt /* session_id */);

  EXPECT_TRUE(enrollment_manager()->IsEnrollmentInProgress());
  VerifyEnrollmentManagerObserversNotifiedOfStart(
      1 /* expected_num_enrollment_started_notifications */);

  CryptAuthEnrollmentResult expected_enrollment_result(
      CryptAuthEnrollmentResult::ResultCode::kSuccessNewKeysEnrolled,
      cryptauthv2::GetClientDirectiveForTest());

  FinishEnrollmentAttempt(
      0u /* expected_enroller_instance_index */,
      cryptauthv2::BuildClientMetadata(
          0 /* retry_count */,
          cryptauthv2::ClientMetadata::INITIALIZATION /* invocation_reason */,
          std::nullopt /* session_id */) /* expected_client_metadata */,
      cryptauthv2::GetClientAppMetadataForTest(), expected_enrollment_result);

  VerifyEnrollmentResults({expected_enrollment_result});
}

TEST_F(DeviceSyncCryptAuthV2EnrollmentManagerImplTest, ForcedEnrollment) {
  CreateEnrollmentManager(cryptauthv2::GetClientAppMetadataForTest());
  enrollment_manager()->Start();

  enrollment_manager()->ForceEnrollmentNow(
      cryptauth::InvocationReason::INVOCATION_REASON_FEATURE_TOGGLED,
      kFakeSessionId);
  EXPECT_TRUE(enrollment_manager()->IsEnrollmentInProgress());
  VerifyEnrollmentManagerObserversNotifiedOfStart(
      1 /* expected_num_enrollment_started_notifications */);

  // Simulate a failed enrollment attempt due to CryptAuth server overload.
  CryptAuthEnrollmentResult expected_enrollment_result(
      CryptAuthEnrollmentResult::ResultCode::kErrorCryptAuthServerOverloaded,
      std::nullopt /* client_directive */);

  FinishEnrollmentAttempt(
      0u /* expected_enroller_instance_index */,
      cryptauthv2::BuildClientMetadata(
          0 /* retry_count */,
          cryptauthv2::ClientMetadata::FEATURE_TOGGLED /* invocation_reason */,
          kFakeSessionId) /* expected_client_metadata */,
      cryptauthv2::GetClientAppMetadataForTest(), expected_enrollment_result);

  VerifyEnrollmentResults({expected_enrollment_result});
}

TEST_F(DeviceSyncCryptAuthV2EnrollmentManagerImplTest,
       RetryAfterFailedPeriodicEnrollment_PreviouslyEnrolled) {
  CreateEnrollmentManager(cryptauthv2::GetClientAppMetadataForTest());

  // The user has already enrolled.
  CryptAuthKey user_key_pair_v2(
      kFakeV2PublicKey, kFakeV2PrivateKey, CryptAuthKey::Status::kActive,
      cryptauthv2::KeyType::P256, kCryptAuthFixedUserKeyPairHandle);
  key_registry()->AddKey(CryptAuthKeyBundle::Name::kUserKeyPair,
                         user_key_pair_v2);
  base::Time expected_last_enrollment_time = test_clock()->Now();
  fake_enrollment_scheduler()->set_last_successful_enrollment_time(
      expected_last_enrollment_time);
  EXPECT_EQ(expected_last_enrollment_time,
            enrollment_manager()->GetLastEnrollmentTime());
  fake_enrollment_scheduler()->set_refresh_period(kFakeRefreshPeriod);
  fake_enrollment_scheduler()->set_time_to_next_enrollment_request(
      kFakeRefreshPeriod);
  EXPECT_TRUE(enrollment_manager()->IsEnrollmentValid());

  enrollment_manager()->Start();

  EXPECT_EQ(kFakeRefreshPeriod, enrollment_manager()->GetTimeToNextAttempt());

  // Now, user is due for a refresh.
  test_clock()->SetNow(test_clock()->Now() + kFakeRefreshPeriod +
                       base::Seconds(1));
  EXPECT_FALSE(enrollment_manager()->IsEnrollmentValid());
  base::TimeDelta expected_time_to_next_attempt = base::Seconds(0);
  fake_enrollment_scheduler()->set_time_to_next_enrollment_request(
      expected_time_to_next_attempt);
  EXPECT_EQ(expected_time_to_next_attempt,
            enrollment_manager()->GetTimeToNextAttempt());

  cryptauthv2::ClientMetadata expected_client_metadata =
      cryptauthv2::BuildClientMetadata(0 /* retry_count */,
                                       cryptauthv2::ClientMetadata::PERIODIC,
                                       std::nullopt /* session_id */);

  // First enrollment attempt fails.
  // Note: User does not yet have a GCM registration ID or ClientAppMetadata.
  fake_enrollment_scheduler()->RequestEnrollment(
      cryptauthv2::ClientMetadata::PERIODIC, std::nullopt /* session_id */);
  EXPECT_TRUE(enrollment_manager()->IsEnrollmentInProgress());
  VerifyEnrollmentManagerObserversNotifiedOfStart(
      1 /* expected_num_enrollment_started_notifications */);

  CryptAuthEnrollmentResult first_expected_enrollment_result(
      CryptAuthEnrollmentResult::ResultCode::kErrorCryptAuthServerOverloaded,
      std::nullopt /* client_directive */);

  FinishEnrollmentAttempt(0u /* expected_enroller_instance_index */,
                          expected_client_metadata,
                          cryptauthv2::GetClientAppMetadataForTest(),
                          first_expected_enrollment_result);

  EXPECT_FALSE(enrollment_manager()->IsRecoveringFromFailure());
  fake_enrollment_scheduler()->set_num_consecutive_enrollment_failures(1);
  EXPECT_TRUE(enrollment_manager()->IsRecoveringFromFailure());

  fake_enrollment_scheduler()->set_time_to_next_enrollment_request(
      kFakeRetryPeriod);
  EXPECT_EQ(kFakeRetryPeriod, enrollment_manager()->GetTimeToNextAttempt());

  // Second (successful) enrollment attempt bypasses GCM registration and
  // ClientAppMetadata fetch because they were performed during the failed
  // attempt.
  test_clock()->SetNow(test_clock()->Now() + kFakeRetryPeriod);
  fake_enrollment_scheduler()->RequestEnrollment(
      cryptauthv2::ClientMetadata::PERIODIC, std::nullopt /* session_id */);
  VerifyEnrollmentManagerObserversNotifiedOfStart(
      2 /* expected_num_enrollment_started_notifications */);

  CryptAuthEnrollmentResult second_expected_enrollment_result =
      CryptAuthEnrollmentResult(
          CryptAuthEnrollmentResult::ResultCode::kSuccessNoNewKeysNeeded,
          cryptauthv2::GetClientDirectiveForTest());

  expected_client_metadata.set_retry_count(1);
  FinishEnrollmentAttempt(1u /* expected_enroller_instance_index */,
                          expected_client_metadata,
                          cryptauthv2::GetClientAppMetadataForTest(),
                          second_expected_enrollment_result);

  VerifyEnrollmentResults(
      {first_expected_enrollment_result, second_expected_enrollment_result});

  fake_enrollment_scheduler()->set_last_successful_enrollment_time(
      test_clock()->Now());
  fake_enrollment_scheduler()->set_time_to_next_enrollment_request(
      kFakeRefreshPeriod);
  fake_enrollment_scheduler()->set_num_consecutive_enrollment_failures(0);
  EXPECT_EQ(test_clock()->Now(), enrollment_manager()->GetLastEnrollmentTime());
  EXPECT_TRUE(enrollment_manager()->IsEnrollmentValid());
  EXPECT_EQ(kFakeRefreshPeriod, enrollment_manager()->GetTimeToNextAttempt());
  EXPECT_FALSE(enrollment_manager()->IsRecoveringFromFailure());
}

TEST_F(DeviceSyncCryptAuthV2EnrollmentManagerImplTest,
       EnrollmentTriggeredByGcmMessage_Success) {
  CreateEnrollmentManager(cryptauthv2::GetClientAppMetadataForTest());
  enrollment_manager()->Start();

  RequestEnrollmentThroughGcm(kFakeSessionId);

  EXPECT_TRUE(enrollment_manager()->IsEnrollmentInProgress());
  VerifyEnrollmentManagerObserversNotifiedOfStart(
      1 /* expected_num_enrollment_started_notifications */);
  CryptAuthEnrollmentResult expected_enrollment_result(
      CryptAuthEnrollmentResult::ResultCode::kSuccessNoNewKeysNeeded,
      cryptauthv2::GetClientDirectiveForTest());
  FinishEnrollmentAttempt(
      0u /* expected_enroller_instance_index */,
      cryptauthv2::BuildClientMetadata(
          0 /* retry_count */, cryptauthv2::ClientMetadata::SERVER_INITIATED,
          kFakeSessionId) /* expected_client_metadata */,
      cryptauthv2::GetClientAppMetadataForTest(), expected_enrollment_result);
  VerifyEnrollmentResults({expected_enrollment_result});
}

TEST_F(DeviceSyncCryptAuthV2EnrollmentManagerImplTest,
       EnrollmentTriggeredByGcmMessage_Failure) {
  CreateEnrollmentManager(cryptauthv2::GetClientAppMetadataForTest());
  enrollment_manager()->Start();

  RequestEnrollmentThroughGcm(kFakeSessionId);

  EXPECT_TRUE(enrollment_manager()->IsEnrollmentInProgress());
  VerifyEnrollmentManagerObserversNotifiedOfStart(
      1 /* expected_num_enrollment_started_notifications */);
  CryptAuthEnrollmentResult expected_enrollment_result(
      CryptAuthEnrollmentResult::ResultCode::kErrorCryptAuthServerOverloaded,
      std::nullopt /* client_directive */);
  FinishEnrollmentAttempt(
      0u /* expected_enroller_instance_index */,
      cryptauthv2::BuildClientMetadata(
          0 /* retry_count */, cryptauthv2::ClientMetadata::SERVER_INITIATED,
          kFakeSessionId) /* expected_client_metadata */,
      cryptauthv2::GetClientAppMetadataForTest(), expected_enrollment_result);
  VerifyEnrollmentResults({expected_enrollment_result});
}

TEST_F(DeviceSyncCryptAuthV2EnrollmentManagerImplTest,
       V1UserKeyPairAddedToRegistryOnConstruction) {
  AddV1UserKeyPairToV1Prefs(kFakeV1PublicKey, kFakeV1PrivateKey);
  CryptAuthKey expected_user_key_pair_v1(
      kFakeV1PublicKey, kFakeV1PrivateKey, CryptAuthKey::Status::kActive,
      cryptauthv2::KeyType::P256, kCryptAuthFixedUserKeyPairHandle);

  EXPECT_FALSE(
      key_registry()->GetActiveKey(CryptAuthKeyBundle::Name::kUserKeyPair));

  CreateEnrollmentManager(cryptauthv2::GetClientAppMetadataForTest());
  histogram_tester()->ExpectBucketCount(
      "CryptAuth.EnrollmentV2.UserKeyPairState",
      1 /* UserKeyPairState::kYesV1KeyNoV2Key */, 1 /* count */);

  EXPECT_EQ(
      expected_user_key_pair_v1,
      *key_registry()->GetActiveKey(CryptAuthKeyBundle::Name::kUserKeyPair));
  EXPECT_EQ(expected_user_key_pair_v1.public_key(),
            enrollment_manager()->GetUserPublicKey());
  EXPECT_EQ(expected_user_key_pair_v1.private_key(),
            enrollment_manager()->GetUserPrivateKey());
}

TEST_F(DeviceSyncCryptAuthV2EnrollmentManagerImplTest,
       V1UserKeyPairOverwritesV2UserKeyPairOnConstruction) {
  AddV1UserKeyPairToV1Prefs(kFakeV1PublicKey, kFakeV1PrivateKey);
  CryptAuthKey expected_user_key_pair_v1(
      kFakeV1PublicKey, kFakeV1PrivateKey, CryptAuthKey::Status::kActive,
      cryptauthv2::KeyType::P256, kCryptAuthFixedUserKeyPairHandle);

  // Add v2 user key pair to registry.
  CryptAuthKey user_key_pair_v2(
      kFakeV2PublicKey, kFakeV2PrivateKey, CryptAuthKey::Status::kActive,
      cryptauthv2::KeyType::P256, kCryptAuthFixedUserKeyPairHandle);
  key_registry()->AddKey(CryptAuthKeyBundle::Name::kUserKeyPair,
                         user_key_pair_v2);
  EXPECT_EQ(user_key_pair_v2, *key_registry()->GetActiveKey(
                                  CryptAuthKeyBundle::Name::kUserKeyPair));

  // A legacy v1 user key pair should overwrite any existing v2 user key pair
  // when the enrollment manager is constructed.
  CreateEnrollmentManager(cryptauthv2::GetClientAppMetadataForTest());
  histogram_tester()->ExpectBucketCount(
      "CryptAuth.EnrollmentV2.UserKeyPairState",
      4 /* UserKeyPairState::kYesV1KeyYesV2KeyDisagree */, 1 /* count */);

  EXPECT_EQ(
      expected_user_key_pair_v1,
      *key_registry()->GetActiveKey(CryptAuthKeyBundle::Name::kUserKeyPair));
  EXPECT_EQ(expected_user_key_pair_v1.public_key(),
            enrollment_manager()->GetUserPublicKey());
  EXPECT_EQ(expected_user_key_pair_v1.private_key(),
            enrollment_manager()->GetUserPrivateKey());

  // Expect re-enrollment using newly added v1 key.
  enrollment_manager()->Start();
  cryptauthv2::ClientMetadata expected_client_metadata =
      cryptauthv2::BuildClientMetadata(
          0 /* retry_count */, cryptauthv2::ClientMetadata::INITIALIZATION,
          std::nullopt /* session_id */);
  CryptAuthEnrollmentResult expected_enrollment_result(
      CryptAuthEnrollmentResult::ResultCode::kSuccessNewKeysEnrolled,
      std::nullopt /* client_directive */);
  FinishEnrollmentAttempt(
      0u /* expected_enroller_instance_index */, expected_client_metadata,
      cryptauthv2::GetClientAppMetadataForTest(), expected_enrollment_result);

  VerifyInvocationReasonHistogram(
      {expected_client_metadata.invocation_reason()});
  VerifyEnrollmentResults({expected_enrollment_result});
}

TEST_F(DeviceSyncCryptAuthV2EnrollmentManagerImplTest,
       V1AndV2UserKeyPairsAgree) {
  AddV1UserKeyPairToV1Prefs(kFakeV1PublicKey, kFakeV1PrivateKey);
  CryptAuthKey user_key_pair_v1(
      kFakeV1PublicKey, kFakeV1PrivateKey, CryptAuthKey::Status::kActive,
      cryptauthv2::KeyType::P256, kCryptAuthFixedUserKeyPairHandle);

  key_registry()->AddKey(CryptAuthKeyBundle::Name::kUserKeyPair,
                         user_key_pair_v1);

  CreateEnrollmentManager(cryptauthv2::GetClientAppMetadataForTest());
  histogram_tester()->ExpectBucketCount(
      "CryptAuth.EnrollmentV2.UserKeyPairState",
      3 /* UserKeyPairState::kYesV1KeyYesV2KeyAgree */, 1 /* count */);

  EXPECT_EQ(user_key_pair_v1, *key_registry()->GetActiveKey(
                                  CryptAuthKeyBundle::Name::kUserKeyPair));
  EXPECT_EQ(user_key_pair_v1.public_key(),
            enrollment_manager()->GetUserPublicKey());
  EXPECT_EQ(user_key_pair_v1.private_key(),
            enrollment_manager()->GetUserPrivateKey());
}

TEST_F(DeviceSyncCryptAuthV2EnrollmentManagerImplTest, V2KeyButNoV1Key) {
  CryptAuthKey user_key_pair_v2(
      kFakeV2PublicKey, kFakeV2PrivateKey, CryptAuthKey::Status::kActive,
      cryptauthv2::KeyType::P256, kCryptAuthFixedUserKeyPairHandle);

  key_registry()->AddKey(CryptAuthKeyBundle::Name::kUserKeyPair,
                         user_key_pair_v2);

  CreateEnrollmentManager(cryptauthv2::GetClientAppMetadataForTest());
  histogram_tester()->ExpectBucketCount(
      "CryptAuth.EnrollmentV2.UserKeyPairState",
      2 /* UserKeyPairState::kNoV1KeyYesV2Key */, 1 /* count */);

  EXPECT_EQ(user_key_pair_v2, *key_registry()->GetActiveKey(
                                  CryptAuthKeyBundle::Name::kUserKeyPair));
  EXPECT_EQ(kFakeV2PublicKey, enrollment_manager()->GetUserPublicKey());
  EXPECT_EQ(kFakeV2PrivateKey, enrollment_manager()->GetUserPrivateKey());
}

TEST_F(DeviceSyncCryptAuthV2EnrollmentManagerImplTest, GetUserKeyPair) {
  CreateEnrollmentManager(cryptauthv2::GetClientAppMetadataForTest());
  histogram_tester()->ExpectBucketCount(
      "CryptAuth.EnrollmentV2.UserKeyPairState",
      0 /* UserKeyPairState::kNoV1KeyNoV2Key */, 1 /* count */);

  EXPECT_TRUE(enrollment_manager()->GetUserPublicKey().empty());
  EXPECT_TRUE(enrollment_manager()->GetUserPrivateKey().empty());

  key_registry()->AddKey(
      CryptAuthKeyBundle::Name::kUserKeyPair,
      CryptAuthKey(kFakeV2PublicKey, kFakeV2PrivateKey,
                   CryptAuthKey::Status::kActive, cryptauthv2::KeyType::P256,
                   kCryptAuthFixedUserKeyPairHandle));
  EXPECT_EQ(kFakeV2PublicKey, enrollment_manager()->GetUserPublicKey());
  EXPECT_EQ(kFakeV2PrivateKey, enrollment_manager()->GetUserPrivateKey());
}

TEST_F(DeviceSyncCryptAuthV2EnrollmentManagerImplTest,
       MultipleEnrollmentAttempts) {
  CreateEnrollmentManager(cryptauthv2::GetClientAppMetadataForTest());
  enrollment_manager()->Start();

  std::vector<cryptauthv2::ClientMetadata::InvocationReason>
      expected_invocation_reasons;
  std::vector<CryptAuthEnrollmentResult> expected_enrollment_results;

  // Successfully enroll for the first time.
  expected_invocation_reasons.push_back(
      cryptauthv2::ClientMetadata::INITIALIZATION);
  expected_enrollment_results.emplace_back(
      CryptAuthEnrollmentResult::ResultCode::kSuccessNewKeysEnrolled,
      cryptauthv2::GetClientDirectiveForTest());
  fake_enrollment_scheduler()->RequestEnrollment(
      expected_invocation_reasons.back(), kFakeSessionId);
  VerifyEnrollmentManagerObserversNotifiedOfStart(
      1 /* expected_num_enrollment_started_notifications */);
  FinishEnrollmentAttempt(
      0u /* expected_enroller_instance_index */,
      cryptauthv2::BuildClientMetadata(
          0 /* retry_count */, expected_invocation_reasons.back(),
          kFakeSessionId) /* expected_client_metadata */,
      cryptauthv2::GetClientAppMetadataForTest(),
      expected_enrollment_results.back());

  // Fail periodic refresh twice due to overloaded CryptAuth server.
  expected_invocation_reasons.push_back(cryptauthv2::ClientMetadata::PERIODIC);
  expected_enrollment_results.emplace_back(
      CryptAuthEnrollmentResult::ResultCode::kErrorCryptAuthServerOverloaded,
      cryptauthv2::GetClientDirectiveForTest());
  fake_enrollment_scheduler()->RequestEnrollment(
      expected_invocation_reasons.back(), kFakeSessionId);
  VerifyEnrollmentManagerObserversNotifiedOfStart(
      2 /* expected_num_enrollment_started_notifications */);
  FinishEnrollmentAttempt(
      1u /* expected_enroller_instance_index */,
      cryptauthv2::BuildClientMetadata(
          0 /* retry_count */, expected_invocation_reasons.back(),
          kFakeSessionId) /* expected_client_metadata */,
      cryptauthv2::GetClientAppMetadataForTest(),
      expected_enrollment_results.back());
  fake_enrollment_scheduler()->set_num_consecutive_enrollment_failures(1);
  fake_enrollment_scheduler()->set_time_to_next_enrollment_request(
      kFakeRetryPeriod);

  expected_invocation_reasons.push_back(cryptauthv2::ClientMetadata::PERIODIC);
  expected_enrollment_results.emplace_back(
      CryptAuthEnrollmentResult::ResultCode::kErrorCryptAuthServerOverloaded,
      std::nullopt /* client_directive */);
  fake_enrollment_scheduler()->RequestEnrollment(
      expected_invocation_reasons.back(), kFakeSessionId);
  VerifyEnrollmentManagerObserversNotifiedOfStart(
      3 /* expected_num_enrollment_started_notifications */);
  FinishEnrollmentAttempt(
      2u /* expected_enroller_instance_index */,
      cryptauthv2::BuildClientMetadata(
          1 /* retry_count */, expected_invocation_reasons.back(),
          kFakeSessionId) /* expected_client_metadata */,
      cryptauthv2::GetClientAppMetadataForTest(),
      expected_enrollment_results.back());
  fake_enrollment_scheduler()->set_num_consecutive_enrollment_failures(2);
  fake_enrollment_scheduler()->set_time_to_next_enrollment_request(
      kFakeRetryPeriod);

  // While waiting for retry, force a manual enrollment that succeeds.
  expected_invocation_reasons.push_back(cryptauthv2::ClientMetadata::MANUAL);
  expected_enrollment_results.emplace_back(
      CryptAuthEnrollmentResult::ResultCode::kSuccessNoNewKeysNeeded,
      std::nullopt /* client_directive */);
  enrollment_manager()->ForceEnrollmentNow(cryptauth::INVOCATION_REASON_MANUAL,
                                           std::nullopt /* session_id */);
  VerifyEnrollmentManagerObserversNotifiedOfStart(
      4 /* expected_num_enrollment_started_notifications */);
  FinishEnrollmentAttempt(
      3u /* expected_enroller_instance_index */,
      cryptauthv2::BuildClientMetadata(
          2 /* retry_count */, expected_invocation_reasons.back(),
          std::nullopt /* session_id */) /* expected_client_metadata */,
      cryptauthv2::GetClientAppMetadataForTest(),
      expected_enrollment_results.back());

  VerifyInvocationReasonHistogram(expected_invocation_reasons);
  VerifyEnrollmentResults(expected_enrollment_results);
}

TEST_F(DeviceSyncCryptAuthV2EnrollmentManagerImplTest,
       MissingUserKeyPairRecovery) {
  CreateEnrollmentManager(cryptauthv2::GetClientAppMetadataForTest());

  // The user has already enrolled according to the scheduler, but there is no
  // user key pair because the key registry was corrupted, for instance.
  fake_enrollment_scheduler()->set_last_successful_enrollment_time(
      test_clock()->Now());
  EXPECT_FALSE(enrollment_manager()->IsEnrollmentValid());

  enrollment_manager()->Start();

  cryptauthv2::ClientMetadata expected_client_metadata =
      cryptauthv2::BuildClientMetadata(
          0 /* retry_count */, cryptauthv2::ClientMetadata::FAILURE_RECOVERY,
          std::nullopt /* session_id */);
  CryptAuthEnrollmentResult expected_enrollment_result(
      CryptAuthEnrollmentResult::ResultCode::kSuccessNewKeysEnrolled,
      std::nullopt /* client_directive */);
  FinishEnrollmentAttempt(
      0u /* expected_enroller_instance_index */, expected_client_metadata,
      cryptauthv2::GetClientAppMetadataForTest(), expected_enrollment_result);

  VerifyInvocationReasonHistogram(
      {expected_client_metadata.invocation_reason()});
  VerifyEnrollmentResults({expected_enrollment_result});
}

TEST_F(DeviceSyncCryptAuthV2EnrollmentManagerImplTest,
       ClientAppMetadataChange) {
  // If the user has never enrolled, no forced enrollment should be made due to
  // a ClientAppMetadata change.
  CreateEnrollmentManager(cryptauthv2::GetClientAppMetadataForTest());
  enrollment_manager()->Start();
  EXPECT_FALSE(enrollment_manager()->IsEnrollmentInProgress());

  // Succeed initialization and persist the client app metadata
  fake_enrollment_scheduler()->RequestEnrollment(
      cryptauthv2::ClientMetadata::INITIALIZATION /* invocation_reason */,
      std::nullopt /* session_id */);
  CryptAuthEnrollmentResult expected_enrollment_result1(
      CryptAuthEnrollmentResult::ResultCode::kSuccessNewKeysEnrolled,
      cryptauthv2::GetClientDirectiveForTest());
  FinishEnrollmentAttempt(
      0u /* expected_enroller_instance_index */,
      cryptauthv2::BuildClientMetadata(
          0 /* retry_count */,
          cryptauthv2::ClientMetadata::INITIALIZATION /* invocation_reason */,
          std::nullopt /* session_id */) /* expected_client_metadata */,
      cryptauthv2::GetClientAppMetadataForTest(), expected_enrollment_result1);
  VerifyEnrollmentResults({expected_enrollment_result1});

  // The client app metadata doesn't change, so don't force an enrollment.
  DestroyEnrollmentManager();
  CreateEnrollmentManager(cryptauthv2::GetClientAppMetadataForTest());
  enrollment_manager()->Start();
  EXPECT_FALSE(enrollment_manager()->IsEnrollmentInProgress());

  // The client app metadata changes, force an enrollment.
  DestroyEnrollmentManager();
  cryptauthv2::ClientAppMetadata new_client_app_metadata =
      cryptauthv2::GetClientAppMetadataForTest();
  new_client_app_metadata.set_instance_id("new_instance_id");
  CreateEnrollmentManager(new_client_app_metadata);
  enrollment_manager()->Start();
  EXPECT_TRUE(enrollment_manager()->IsEnrollmentInProgress());

  // Enrollment fails; new client app metadata not persisted.
  CryptAuthEnrollmentResult expected_enrollment_result2(
      CryptAuthEnrollmentResult::ResultCode::kErrorCryptAuthServerOverloaded,
      std::nullopt /* client_directive */);
  FinishEnrollmentAttempt(
      1u /* expected_enroller_instance_index */,
      cryptauthv2::BuildClientMetadata(
          0 /* retry_count */,
          cryptauthv2::ClientMetadata::SOFTWARE_UPDATE /* invocation_reason */,
          std::nullopt /* session_id */) /* expected_client_metadata */,
      new_client_app_metadata, expected_enrollment_result2);
  VerifyEnrollmentResults(
      {expected_enrollment_result1, expected_enrollment_result2});

  // We still haven't successfully enrolled the new client app metadata; on
  // start-up we still see a metadata change and force the enrollment.
  DestroyEnrollmentManager();
  CreateEnrollmentManager(new_client_app_metadata);
  enrollment_manager()->Start();
  EXPECT_TRUE(enrollment_manager()->IsEnrollmentInProgress());

  // Enrollment succeeds; new client app metadata is persisted.
  CryptAuthEnrollmentResult expected_enrollment_result3(
      CryptAuthEnrollmentResult::ResultCode::kSuccessNewKeysEnrolled,
      std::nullopt /* client_directive */);
  FinishEnrollmentAttempt(
      2u /* expected_enroller_instance_index */,
      cryptauthv2::BuildClientMetadata(
          0 /* retry_count */,
          cryptauthv2::ClientMetadata::SOFTWARE_UPDATE /* invocation_reason */,
          std::nullopt /* session_id */) /* expected_client_metadata */,
      new_client_app_metadata, expected_enrollment_result3);
  VerifyEnrollmentResults({expected_enrollment_result1,
                           expected_enrollment_result2,
                           expected_enrollment_result3});

  // We have successfully enrolled the new client app metadata; no change
  // detected on start-up.
  DestroyEnrollmentManager();
  CreateEnrollmentManager(new_client_app_metadata);
  enrollment_manager()->Start();
  EXPECT_FALSE(enrollment_manager()->IsEnrollmentInProgress());
}

}  // namespace device_sync

}  // namespace ash