chromium/ios/chrome/browser/credential_provider/model/credential_provider_service_unittest.mm

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/credential_provider/model/credential_provider_service.h"

#import <memory>
#import <string>
#import <utility>
#import <vector>

#import "base/location.h"
#import "base/memory/scoped_refptr.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_feature_list.h"
#import "base/test/task_environment.h"
#import "components/affiliations/core/browser/fake_affiliation_service.h"
#import "components/favicon/core/large_icon_service.h"
#import "components/password_manager/core/browser/password_form.h"
#import "components/password_manager/core/browser/password_store/password_store_change.h"
#import "components/password_manager/core/browser/password_store/test_password_store.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "components/password_manager/core/common/password_manager_pref_names.h"
#import "components/prefs/pref_registry_simple.h"
#import "components/prefs/pref_service.h"
#import "components/prefs/testing_pref_service.h"
#import "components/signin/public/identity_manager/identity_test_environment.h"
#import "components/sync/base/user_selectable_type.h"
#import "components/sync/test/test_sync_service.h"
#import "components/webauthn/core/browser/test_passkey_model.h"
#import "ios/chrome/browser/credential_provider/model/credential_provider_util.h"
#import "ios/chrome/browser/credential_provider/model/features.h"
#import "ios/chrome/browser/favicon/model/favicon_loader.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
#import "ios/chrome/common/credential_provider/constants.h"
#import "ios/chrome/common/credential_provider/credential.h"
#import "ios/chrome/common/credential_provider/memory_credential_store.h"
#import "testing/gmock/include/gmock/gmock.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"

using testing::_;

namespace {

using testing::UnorderedElementsAre;

// Extracts the service names of `credentials` to an std::vector, so tests can
// use a gmock matcher on it.
std::vector<std::string> GetServiceNames(NSArray<id<Credential>>* credentials) {
  std::vector<std::string> service_names;
  for (id<Credential> credential : credentials) {
    service_names.push_back(base::SysNSStringToUTF8(credential.serviceName));
  }
  return service_names;
}

sync_pb::WebauthnCredentialSpecifics CreatePasskey(
    const std::string& rp_id,
    const std::string& user_id,
    const std::string& user_name,
    const std::string& user_display_name) {
  sync_pb::WebauthnCredentialSpecifics passkey;
  passkey.set_sync_id(base::RandBytesAsString(16));
  passkey.set_credential_id(base::RandBytesAsString(16));
  passkey.set_rp_id(rp_id);
  passkey.set_user_id(user_id);
  passkey.set_user_name(user_name);
  passkey.set_user_display_name(user_display_name);
  return passkey;
}

// Needed since FaviconLoader has no fake currently.
class MockLargeIconService : public favicon::LargeIconService {
 public:
  MOCK_METHOD(base::CancelableTaskTracker::TaskId,
              GetLargeIconRawBitmapOrFallbackStyleForPageUrl,
              (const GURL&,
               int,
               int,
               favicon_base::LargeIconCallback,
               base::CancelableTaskTracker*),
              (override));
  MOCK_METHOD(base::CancelableTaskTracker::TaskId,
              GetLargeIconImageOrFallbackStyleForPageUrl,
              (const GURL&,
               int,
               int,
               favicon_base::LargeIconImageCallback,
               base::CancelableTaskTracker*),
              (override));
  MOCK_METHOD(base::CancelableTaskTracker::TaskId,
              GetLargeIconRawBitmapForPageUrl,
              (const GURL&,
               int,
               std::optional<int>,
               LargeIconService::NoBigEnoughIconBehavior,
               favicon_base::LargeIconCallback,
               base::CancelableTaskTracker*),
              (override));
  MOCK_METHOD(base::CancelableTaskTracker::TaskId,
              GetLargeIconRawBitmapOrFallbackStyleForIconUrl,
              (const GURL&,
               int,
               int,
               favicon_base::LargeIconCallback,
               base::CancelableTaskTracker*),
              (override));
  MOCK_METHOD(base::CancelableTaskTracker::TaskId,
              GetIconRawBitmapOrFallbackStyleForPageUrl,
              (const GURL&,
               int,
               favicon_base::LargeIconCallback,
               base::CancelableTaskTracker*),
              (override));
  MOCK_METHOD(void,
              GetLargeIconOrFallbackStyleFromGoogleServerSkippingLocalCache,
              (const GURL&,
               bool,
               const net::NetworkTrafficAnnotationTag&,
               favicon_base::GoogleFaviconServerCallback),
              (override));
  MOCK_METHOD(void,
              GetLargeIconFromCacheFallbackToGoogleServer,
              (const GURL& page_url,
               StandardIconSize min_source_size_in_pixel,
               std::optional<StandardIconSize> size_in_pixel_to_resize_to,
               NoBigEnoughIconBehavior no_big_enough_icon_behavior,
               bool should_trim_page_url_path,
               const net::NetworkTrafficAnnotationTag& traffic_annotation,
               favicon_base::LargeIconCallback callback,
               base::CancelableTaskTracker* tracker),
              (override));
  MOCK_METHOD(void, TouchIconFromGoogleServer, (const GURL&), (override));
};

class CredentialProviderServiceTest : public PlatformTest {
 public:
  CredentialProviderServiceTest() = default;
  ~CredentialProviderServiceTest() override = default;

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

  void SetUp() override {
    PlatformTest::SetUp();
    // Make sure there are no favicons left from some other tests.
    EXPECT_TRUE(DeleteFaviconsFolder());
    password_store_->Init(&testing_pref_service_,
                          /*affiliated_match_helper=*/nullptr);
    account_password_store_->Init(&testing_pref_service_,
                                  /*affiliated_match_helper=*/nullptr);
    testing_pref_service_.registry()->RegisterBooleanPref(
        password_manager::prefs::kCredentialsEnableService, true);
  }

  void TearDown() override {
    // Delete all favicon files that were created during the test.
    EXPECT_TRUE(DeleteFaviconsFolder());
    credential_provider_service_->Shutdown();
    password_store_->ShutdownOnUIThread();
    account_password_store_->ShutdownOnUIThread();
    PlatformTest::TearDown();
  }

  void CreateCredentialProviderService(bool with_account_store = false) {
    credential_provider_service_ = std::make_unique<CredentialProviderService>(
        &testing_pref_service_, password_store_,
        with_account_store ? account_password_store_ : nullptr,
        test_passkey_model_.get(), credential_store_,
        identity_test_environment_.identity_manager(), &sync_service_,
        &affiliation_service_, &favicon_loader_);
  }

 protected:
  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  TestingPrefServiceSimple testing_pref_service_;
  scoped_refptr<password_manager::TestPasswordStore> password_store_ =
      base::MakeRefCounted<password_manager::TestPasswordStore>();
  scoped_refptr<password_manager::TestPasswordStore> account_password_store_ =
      base::MakeRefCounted<password_manager::TestPasswordStore>(
          password_manager::IsAccountStore(true));
  std::unique_ptr<webauthn::TestPasskeyModel> test_passkey_model_ =
      std::make_unique<webauthn::TestPasskeyModel>();
  MemoryCredentialStore* credential_store_ =
      [[MemoryCredentialStore alloc] init];
  signin::IdentityTestEnvironment identity_test_environment_;
  syncer::TestSyncService sync_service_;
  affiliations::FakeAffiliationService affiliation_service_;
  MockLargeIconService large_icon_service_;
  FaviconLoader favicon_loader_ = FaviconLoader(&large_icon_service_);
  std::unique_ptr<CredentialProviderService> credential_provider_service_;
};

// Test that CredentialProviderService writes all the credentials the first time
// it runs.
TEST_F(CredentialProviderServiceTest, FirstSync) {
  password_manager::PasswordForm form;
  form.url = GURL("http://g.com");
  form.username_value = u"user";
  form.password_value = u"qwerty123";
  password_store_->AddLogin(form);
  base::RunLoop().RunUntilIdle();

  CreateCredentialProviderService();
  // The first write is delayed.
  task_environment_.FastForwardBy(base::Seconds(30));
  base::RunLoop().RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 1u);
  EXPECT_NSEQ(credential_store_.credentials[0].serviceName, @"g.com");
  EXPECT_NSEQ(credential_store_.credentials[0].username, @"user");
  EXPECT_NSEQ(credential_store_.credentials[0].password, @"qwerty123");
}

TEST_F(CredentialProviderServiceTest, TwoStores) {
  password_manager::PasswordForm local_form;
  local_form.url = GURL("http://local.com");
  local_form.username_value = u"user";
  local_form.keychain_identifier = "encrypted-pwd";
  password_store_->AddLogin(local_form);
  password_manager::PasswordForm account_form = local_form;
  account_form.url = GURL("http://account.com");
  account_password_store_->AddLogin(account_form);
  CreateCredentialProviderService(/*with_account_store=*/true);
  base::RunLoop().RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 2u);
  EXPECT_THAT(GetServiceNames(credential_store_.credentials),
              UnorderedElementsAre("local.com", "account.com"));

  password_manager::PasswordForm local_and_account_form = local_form;
  local_and_account_form.url = GURL("http://local-and-account.com");
  password_store_->AddLogin(local_and_account_form);
  account_password_store_->AddLogin(local_and_account_form);
  base::RunLoop().RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 3u);
  EXPECT_THAT(GetServiceNames(credential_store_.credentials),
              UnorderedElementsAre("local.com", "account.com",
                                   "local-and-account.com"));

  password_store_->RemoveLogin(FROM_HERE, local_and_account_form);
  base::RunLoop().RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 3u);
  EXPECT_THAT(GetServiceNames(credential_store_.credentials),
              UnorderedElementsAre("local.com", "account.com",
                                   "local-and-account.com"));

  account_password_store_->RemoveLogin(FROM_HERE, local_and_account_form);
  base::RunLoop().RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 2u);
  EXPECT_THAT(GetServiceNames(credential_store_.credentials),
              UnorderedElementsAre("local.com", "account.com"));
}

// Test that CredentialProviderService observes changes in the password store.
TEST_F(CredentialProviderServiceTest, PasswordChanges) {
  CreateCredentialProviderService();

  EXPECT_EQ(0u, credential_store_.credentials.count);

  password_manager::PasswordForm form;
  form.url = GURL("http://0.com");
  form.signon_realm = "http://www.example.com/";
  form.action = GURL("http://www.example.com/action");
  form.password_element = u"pwd";
  form.password_value = u"qwerty123";
  password_store_->AddLogin(form);
  task_environment_.RunUntilIdle();

  // Expect the store to be populated with 1 credential.
  ASSERT_EQ(1u, credential_store_.credentials.count);

  form.password_value = u"Qwerty123!";
  password_store_->UpdateLogin(form);
  task_environment_.RunUntilIdle();

  // Expect that the credential in the store now has the same password.
  ASSERT_EQ(1u, credential_store_.credentials.count);
  EXPECT_NSEQ(credential_store_.credentials[0].password, @"Qwerty123!");

  password_store_->RemoveLogin(FROM_HERE, form);
  task_environment_.RunUntilIdle();

  // Expect the store to be empty.
  EXPECT_EQ(0u, credential_store_.credentials.count);
}

// Test that CredentialProviderService observes changes in the primary identity.
TEST_F(CredentialProviderServiceTest, AccountChange) {
  CreateCredentialProviderService();

  EXPECT_FALSE([app_group::GetGroupUserDefaults()
      stringForKey:AppGroupUserDefaultsCredentialProviderUserID()]);

  // Enable sync for managed account.
  CoreAccountInfo core_account =
      identity_test_environment_.MakeAccountAvailable("[email protected]");
  AccountInfo account;
  account.account_id = core_account.account_id;
  account.gaia = core_account.gaia;
  account.email = core_account.email;
  account.hosted_domain = "managed.com";
  ASSERT_TRUE(account.IsManaged());
  identity_test_environment_.UpdateAccountInfoForAccount(account);
  identity_test_environment_.SetPrimaryAccount("[email protected]",
                                               signin::ConsentLevel::kSync);
  base::RunLoop().RunUntilIdle();

  EXPECT_NSEQ([app_group::GetGroupUserDefaults()
                  stringForKey:AppGroupUserDefaultsCredentialProviderUserID()],
              base::SysUTF8ToNSString(core_account.gaia));

  identity_test_environment_.ClearPrimaryAccount();
  base::RunLoop().RunUntilIdle();

  EXPECT_FALSE([app_group::GetGroupUserDefaults()
      stringForKey:AppGroupUserDefaultsCredentialProviderUserID()]);
}

// Test that CredentialProviderService observes changes in the password store.
TEST_F(CredentialProviderServiceTest, AndroidCredential) {
  CreateCredentialProviderService();

  EXPECT_EQ(0u, credential_store_.credentials.count);

  password_manager::PasswordForm form;
  form.url = GURL(form.signon_realm);
  form.signon_realm = "android://[email protected]";
  form.password_element = u"pwd";
  form.password_value = u"example";
  password_store_->AddLogin(form);
  task_environment_.RunUntilIdle();

  // Expect the store to be populated with 1 credential.
  EXPECT_EQ(1u, credential_store_.credentials.count);
}

// Test that the CredentialProviderService observes changes in the preference
// that controls password creation
TEST_F(CredentialProviderServiceTest, PasswordCreationPreference) {
  CreateCredentialProviderService();

  // The test is initialized with the preference as true. Make sure the
  // NSUserDefaults value is also true.
  EXPECT_TRUE([[app_group::GetGroupUserDefaults()
      objectForKey:
          AppGroupUserDefaulsCredentialProviderSavingPasswordsEnabled()]
      boolValue]);

  // Change the pref value to false.
  testing_pref_service_.SetBoolean(
      password_manager::prefs::kCredentialsEnableService, false);

  // Make sure the NSUserDefaults value is now false.
  EXPECT_FALSE([[app_group::GetGroupUserDefaults()
      objectForKey:
          AppGroupUserDefaulsCredentialProviderSavingPasswordsEnabled()]
      boolValue]);
}

// Tests that the CredentialProviderService has the correct stored email based
// on the password sync state.
TEST_F(CredentialProviderServiceTest, PasswordSyncStoredEmail) {
  // Start by signing in and turning sync on.
  CoreAccountInfo account;
  account.email = "[email protected]";
  account.gaia = "gaia";
  account.account_id = CoreAccountId::FromGaiaId("gaia");
  sync_service_.SetSignedIn(signin::ConsentLevel::kSync, account);

  CreateCredentialProviderService();

  EXPECT_NSEQ(
      @"[email protected]",
      [app_group::GetGroupUserDefaults()
          stringForKey:AppGroupUserDefaultsCredentialProviderUserEmail()]);

  // Turn off password sync.
  syncer::UserSelectableTypeSet user_selectable_type_set =
      sync_service_.GetUserSettings()->GetSelectedTypes();
  user_selectable_type_set.Remove(syncer::UserSelectableType::kPasswords);
  sync_service_.GetUserSettings()->SetSelectedTypes(
      /*sync_everything=*/false,
      /*types=*/user_selectable_type_set);
  sync_service_.FireStateChanged();

  EXPECT_FALSE([app_group::GetGroupUserDefaults()
      stringForKey:AppGroupUserDefaultsCredentialProviderUserEmail()]);
}

// Tests that the CredentialProviderService has the correct stored email based
// on the account storage state.
TEST_F(CredentialProviderServiceTest, SignedInUserStoredEmail) {
  // Set up a signed in user with the flag enabled.
  CoreAccountInfo account;
  account.email = "[email protected]";
  account.gaia = "gaia";
  account.account_id = CoreAccountId::FromGaiaId("gaia");
  sync_service_.SetSignedIn(signin::ConsentLevel::kSignin, account);

  CreateCredentialProviderService();

  EXPECT_NSEQ(
      [app_group::GetGroupUserDefaults()
          stringForKey:AppGroupUserDefaultsCredentialProviderUserEmail()],
      @"[email protected]");

  // Disable account storage.
  syncer::UserSelectableTypeSet user_selectable_type_set =
      sync_service_.GetUserSettings()->GetSelectedTypes();
  user_selectable_type_set.Remove(syncer::UserSelectableType::kPasswords);
  sync_service_.GetUserSettings()->SetSelectedTypes(
      /*sync_everything=*/false,
      /*types=*/user_selectable_type_set);
  sync_service_.FireStateChanged();

  EXPECT_FALSE([app_group::GetGroupUserDefaults()
      stringForKey:AppGroupUserDefaultsCredentialProviderUserEmail()]);
}

TEST_F(CredentialProviderServiceTest, AddCredentialsWithValidURL) {
  CreateCredentialProviderService();

  ASSERT_EQ(credential_store_.credentials.count, 0u);

  // Add password with valid URL to store.
  EXPECT_CALL(large_icon_service_,
              GetLargeIconRawBitmapOrFallbackStyleForPageUrl(_, _, _, _, _))
      .Times(1);
  password_manager::PasswordForm valid_password_form;
  valid_password_form.url = GURL("http://g.com");
  valid_password_form.username_value = u"user1";
  valid_password_form.password_value = u"pwd1";
  password_store_->AddLogin(valid_password_form);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 1u);

  // Don't add password with invalid URL to store.
  // No favicon should be fetched for invalid URLs.
  EXPECT_CALL(large_icon_service_,
              GetLargeIconRawBitmapOrFallbackStyleForPageUrl(_, _, _, _, _))
      .Times(0);
  password_manager::PasswordForm invalid_password_form;
  invalid_password_form.url = GURL("");
  invalid_password_form.username_value = u"user2";
  invalid_password_form.password_value = u"pwd2";
  password_store_->AddLogin(invalid_password_form);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 1u);

  // Add password with valid Android facet URI to store.
  // No favicon should be fetched for Android URI.
  EXPECT_CALL(large_icon_service_,
              GetLargeIconRawBitmapOrFallbackStyleForPageUrl(_, _, _, _, _))
      .Times(0);
  password_manager::PasswordForm android_password_form;
  android_password_form.url = GURL(android_password_form.signon_realm);
  android_password_form.signon_realm = "android://[email protected]";
  android_password_form.password_element = u"pwd";
  android_password_form.password_value = u"example";
  password_store_->AddLogin(android_password_form);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 2u);
}

TEST_F(CredentialProviderServiceTest, AddCredentialsRefactored) {
  base::test::ScopedFeatureList scoped_feature_list_;
  scoped_feature_list_.InitWithFeatureState(
      kCredentialProviderPerformanceImprovements, true);

  CreateCredentialProviderService();
  ASSERT_EQ(credential_store_.credentials.count, 0u);

  // Add password with valid URL to store.
  EXPECT_CALL(large_icon_service_,
              GetLargeIconRawBitmapOrFallbackStyleForPageUrl(_, _, _, _, _))
      .Times(1);
  password_manager::PasswordForm valid_password_form;
  valid_password_form.url = GURL("http://g.com");
  valid_password_form.username_value = u"user1";
  valid_password_form.password_value = u"pwd1";
  password_store_->AddLogin(valid_password_form);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 1u);

  // Don't add password with invalid URL to store.
  // No favicon should be fetched for invalid URLs.
  EXPECT_CALL(large_icon_service_,
              GetLargeIconRawBitmapOrFallbackStyleForPageUrl(_, _, _, _, _))
      .Times(0);
  password_manager::PasswordForm invalid_password_form;
  invalid_password_form.url = GURL("");
  invalid_password_form.username_value = u"user2";
  invalid_password_form.password_value = u"pwd2";
  password_store_->AddLogin(invalid_password_form);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 1u);

  // Add password with valid Android facet URI to store.
  // No favicon should be fetched for Android URI.
  EXPECT_CALL(large_icon_service_,
              GetLargeIconRawBitmapOrFallbackStyleForPageUrl(_, _, _, _, _))
      .Times(0);
  password_manager::PasswordForm android_password_form;
  android_password_form.url = GURL(android_password_form.signon_realm);
  android_password_form.signon_realm = "android://[email protected]";
  android_password_form.password_element = u"pwd";
  android_password_form.password_value = u"example";
  password_store_->AddLogin(android_password_form);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 2u);
}

TEST_F(CredentialProviderServiceTest,
       OnLoginsChanged_WithPerformanceImprovements_SingleOperation) {
  base::test::ScopedFeatureList scoped_feature_list_;
  scoped_feature_list_.InitWithFeatureState(
      kCredentialProviderPerformanceImprovements, true);

  CreateCredentialProviderService();
  ASSERT_EQ(credential_store_.credentials.count, 0u);

  base::HistogramTester histogram_tester;

  // Test adding a password.
  password_manager::PasswordForm test_form;
  test_form.url = GURL("http://example.com/login");
  test_form.username_value = u"username";
  test_form.password_value = u"12345";

  password_manager::PasswordStoreChangeList change_list;
  change_list.emplace_back(password_manager::PasswordStoreChange(
      password_manager::PasswordStoreChange::ADD, test_form));

  credential_provider_service_->OnLoginsChanged(password_store_.get(),
                                                change_list);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 1u);
  EXPECT_NSEQ(credential_store_.credentials[0].username, @"username");
  EXPECT_NSEQ(credential_store_.credentials[0].password, @"12345");
  histogram_tester.ExpectTotalCount(kSyncStoreHistogramName, 1);

  // Test updating a password.
  test_form.password_value = u"54321";
  change_list.clear();
  password_manager::PasswordStoreChange change(
      password_manager::PasswordStoreChange::UPDATE, test_form,
      /*password_changed=*/true);
  change_list.emplace_back(change);

  credential_provider_service_->OnLoginsChanged(password_store_.get(),
                                                change_list);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 1u);
  EXPECT_NSEQ(credential_store_.credentials[0].password, @"54321");
  histogram_tester.ExpectTotalCount(kSyncStoreHistogramName, 2);

  // Test deleting a password.
  change_list.clear();
  change_list.emplace_back(password_manager::PasswordStoreChange(
      password_manager::PasswordStoreChange::REMOVE, test_form));

  credential_provider_service_->OnLoginsChanged(password_store_.get(),
                                                change_list);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 0u);
  histogram_tester.ExpectTotalCount(kSyncStoreHistogramName, 3);
}

TEST_F(CredentialProviderServiceTest,
       OnLoginsChanged_WithPerformanceImprovements_MultipleOperations) {
  base::test::ScopedFeatureList scoped_feature_list_;
  scoped_feature_list_.InitWithFeatureState(
      kCredentialProviderPerformanceImprovements, true);

  CreateCredentialProviderService();
  ASSERT_EQ(credential_store_.credentials.count, 0u);

  // Setup
  password_manager::PasswordForm test_form;
  test_form.url = GURL("http://example.com/login");
  test_form.username_value = u"username";
  test_form.password_value = u"12345";

  password_manager::PasswordForm test_form2;
  test_form2.url = GURL("http://homersimpson.com/login");
  test_form2.username_value = u"homer";
  test_form2.password_value = u"simpson";

  password_manager::PasswordStoreChangeList change_list;
  change_list.emplace_back(password_manager::PasswordStoreChange(
      password_manager::PasswordStoreChange::ADD, test_form));
  change_list.emplace_back(password_manager::PasswordStoreChange(
      password_manager::PasswordStoreChange::ADD, test_form2));

  credential_provider_service_->OnLoginsChanged(password_store_.get(),
                                                change_list);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 2u);

  // Prepare simultaneous ADD, UPDATE and REMOVE operations.
  password_manager::PasswordForm test_form3;
  test_form3.url = GURL("http://margesimpson.com/login");
  test_form3.username_value = u"marge";
  test_form3.password_value = u"bouvier";

  test_form2.password_value = u"JSimpson";

  change_list.clear();
  change_list.emplace_back(password_manager::PasswordStoreChange(
      password_manager::PasswordStoreChange::ADD, test_form3));
  change_list.emplace_back(password_manager::PasswordStoreChange(
      password_manager::PasswordStoreChange::UPDATE, test_form2,
      /*password_changed=*/true));
  change_list.emplace_back(password_manager::PasswordStoreChange(
      password_manager::PasswordStoreChange::REMOVE, test_form));

  base::HistogramTester histogram_tester;

  // Test results.
  credential_provider_service_->OnLoginsChanged(password_store_.get(),
                                                change_list);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 2u);
  EXPECT_NSEQ(credential_store_.credentials[0].username, @"homer");
  EXPECT_NSEQ(credential_store_.credentials[0].password, @"JSimpson");
  EXPECT_NSEQ(credential_store_.credentials[1].username, @"marge");
  EXPECT_NSEQ(credential_store_.credentials[1].password, @"bouvier");

  // There should have been only one write to disk.
  histogram_tester.ExpectTotalCount(kSyncStoreHistogramName, 1);
}

// Tests that a PasswordStoreChange Update that doesn't change the password
// doesn't result in a write to disk.
TEST_F(
    CredentialProviderServiceTest,
    OnLoginsChanged_WithPerformanceImprovements_UpdateWithoutPasswordChangeNoDiskSave) {
  base::test::ScopedFeatureList scoped_feature_list_;
  scoped_feature_list_.InitWithFeatureState(
      kCredentialProviderPerformanceImprovements, true);

  CreateCredentialProviderService();
  ASSERT_EQ(credential_store_.credentials.count, 0u);

  // Setup
  password_manager::PasswordForm test_form;
  test_form.url = GURL("http://example.com/login");
  test_form.username_value = u"username";
  test_form.password_value = u"12345";

  password_manager::PasswordStoreChangeList change_list;
  change_list.emplace_back(password_manager::PasswordStoreChange(
      password_manager::PasswordStoreChange::ADD, test_form));

  credential_provider_service_->OnLoginsChanged(password_store_.get(),
                                                change_list);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 1u);

  // Update the PasswordForm without changing its username, url or password.
  // This mimicks a password usage.
  test_form.date_last_used = base::Time::Now();

  change_list.clear();
  change_list.emplace_back(password_manager::PasswordStoreChange(
      password_manager::PasswordStoreChange::UPDATE, test_form,
      /*password_changed=*/false));

  base::HistogramTester histogram_tester;

  // Test results.
  credential_provider_service_->OnLoginsChanged(password_store_.get(),
                                                change_list);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 1u);
  EXPECT_NSEQ(credential_store_.credentials[0].username, @"username");
  EXPECT_NSEQ(credential_store_.credentials[0].password, @"12345");

  // There should have been only one write to disk.
  histogram_tester.ExpectTotalCount(kSyncStoreHistogramName, 0);
}

TEST_F(CredentialProviderServiceTest, AddPasskeys) {
  CreateCredentialProviderService(/*with_account_store=*/true);

  ASSERT_EQ(credential_store_.credentials.count, 0u);

  // Add passkey with valid URL to store.
  EXPECT_CALL(large_icon_service_,
              GetLargeIconRawBitmapOrFallbackStyleForPageUrl(_, _, _, _, _))
      .Times(1);
  sync_pb::WebauthnCredentialSpecifics valid_passkey = CreatePasskey(
      "g.com", {1, 2, 3, 4}, "passkey_username", "passkey_display_name");
  test_passkey_model_->AddNewPasskeyForTesting(valid_passkey);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 1u);

  // Don't add passkey with invalid URL to store.
  // No favicon should be fetched for invalid URLs.
  EXPECT_CALL(large_icon_service_,
              GetLargeIconRawBitmapOrFallbackStyleForPageUrl(_, _, _, _, _))
      .Times(0);
  sync_pb::WebauthnCredentialSpecifics invalid_passkey = CreatePasskey(
      "", {1, 2, 3, 4}, "passkey_username", "passkey_display_name");
  test_passkey_model_->AddNewPasskeyForTesting(invalid_passkey);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 1u);

  // Add 2nd passkey with valid URL to store.
  EXPECT_CALL(large_icon_service_,
              GetLargeIconRawBitmapOrFallbackStyleForPageUrl(_, _, _, _, _))
      .Times(1);
  sync_pb::WebauthnCredentialSpecifics valid_passkey2 = CreatePasskey(
      "g.com", {1, 2, 3, 4}, "passkey_username2", "passkey_display_name2");
  test_passkey_model_->AddNewPasskeyForTesting(valid_passkey2);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 2u);
}

TEST_F(CredentialProviderServiceTest, DeletePasskey) {
  CreateCredentialProviderService(/*with_account_store=*/true);

  ASSERT_EQ(credential_store_.credentials.count, 0u);

  // Add passkey with valid URL to store.
  EXPECT_CALL(large_icon_service_,
              GetLargeIconRawBitmapOrFallbackStyleForPageUrl(_, _, _, _, _))
      .Times(1);
  sync_pb::WebauthnCredentialSpecifics passkey = CreatePasskey(
      "g.com", {1, 2, 3, 4}, "passkey_username", "passkey_display_name");
  test_passkey_model_->AddNewPasskeyForTesting(passkey);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 1u);

  test_passkey_model_->DeletePasskey(passkey.credential_id(), FROM_HERE);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 0u);
}

TEST_F(CredentialProviderServiceTest, UpdatePasskey) {
  CreateCredentialProviderService(/*with_account_store=*/true);

  ASSERT_EQ(credential_store_.credentials.count, 0u);

  // Add passkey with valid URL to store.
  sync_pb::WebauthnCredentialSpecifics passkey = CreatePasskey(
      "g.com", {1, 2, 3, 4}, "passkey_username", "passkey_display_name");
  test_passkey_model_->AddNewPasskeyForTesting(passkey);
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 1u);

  test_passkey_model_->UpdatePasskey(
      passkey.credential_id(),
      {
          .user_name = "new_passkey_username",
          .user_display_name = "new_passkey_display_name",
      });
  task_environment_.RunUntilIdle();

  ASSERT_EQ(credential_store_.credentials.count, 1u);
  EXPECT_NSEQ(credential_store_.credentials[0].username,
              @"new_passkey_username");
  EXPECT_NSEQ(credential_store_.credentials[0].userDisplayName,
              @"new_passkey_display_name");
}

}  // namespace