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

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

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

#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/task_environment.h"
#import "components/password_manager/core/browser/password_form.h"
#import "components/password_manager/core/browser/password_store/mock_password_store_interface.h"
#import "components/webauthn/core/browser/test_passkey_model.h"
#import "ios/chrome/browser/credential_provider/model/archivable_credential+password_form.h"
#import "ios/chrome/common/credential_provider/archivable_credential+passkey.h"
#import "ios/chrome/common/credential_provider/user_defaults_credential_store.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "url/gurl.h"

namespace {

constexpr int64_t kJan1st2024 = 1704085200;

using base::SysNSStringToUTF8;
using base::test::ios::kWaitForFileOperationTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;
using password_manager::MockPasswordStoreInterface;
using password_manager::PasswordForm;
using ::testing::_;

NSData* StringToData(std::string str) {
  return [NSData dataWithBytes:str.data() length:str.length()];
}

ArchivableCredential* TestPasswordCredential() {
  NSString* username = @"username_value";
  NSString* password = @"qwerty123";
  NSString* url = @"http://www.alpha.example.com/path/and?args=8";
  NSString* recordIdentifier = @"recordIdentifier";
  NSString* note = @"note";
  return [[ArchivableCredential alloc] initWithFavicon:nil
                                                  gaia:nil
                                              password:password
                                                  rank:1
                                      recordIdentifier:recordIdentifier
                                     serviceIdentifier:url
                                           serviceName:nil
                                              username:username
                                                  note:note];
}

ArchivableCredential* TestPasskeyCredential() {
  return
      [[ArchivableCredential alloc] initWithFavicon:nil
                                               gaia:nil
                                   recordIdentifier:@"recordIdentifier"
                                             syncId:StringToData("syncId")
                                           username:@"username"
                                    userDisplayName:@"userDisplayName"
                                             userId:StringToData("userId")
                                       credentialId:StringToData("credentialId")
                                               rpId:@"rpId"
                                         privateKey:StringToData("privateKey")
                                          encrypted:StringToData("encrypted")
                                       creationTime:kJan1st2024
                                       lastUsedTime:kJan1st2024];
}

class CredentialProviderMigratorTest : public PlatformTest {
 protected:
  void SetUp() override { [user_defaults_ removeObjectForKey:store_key_]; }
  void TearDown() override { [user_defaults_ removeObjectForKey:store_key_]; }

  NSUserDefaults* user_defaults_ = [NSUserDefaults standardUserDefaults];
  NSString* store_key_ = @"store_key";
  scoped_refptr<MockPasswordStoreInterface> mock_store_ =
      base::MakeRefCounted<testing::NiceMock<MockPasswordStoreInterface>>();
  webauthn::TestPasskeyModel test_passkey_model_;

 private:
  // Mocking time is required for password notes since they are created with the
  // creation_date metadata, which is compared in AddLogin() call expectations.
  base::test::SingleThreadTaskEnvironment task_environment_{
      base::test::TaskEnvironment::MainThreadType::IO,
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
};

// Tests basic migration for 1 password credential.
TEST_F(CredentialProviderMigratorTest, Migration) {
  // Create temp store and add 1 credential.
  UserDefaultsCredentialStore* store =
      [[UserDefaultsCredentialStore alloc] initWithUserDefaults:user_defaults_
                                                            key:store_key_];
  id<Credential> credential = TestPasswordCredential();
  [store addCredential:credential];
  [store saveDataWithCompletion:^(NSError* error) {
    EXPECT_TRUE(error == nil);
  }];
  EXPECT_EQ(store.credentials.count, 1u);

  // Create the migrator to be tested.
  CredentialProviderMigrator* migrator =
      [[CredentialProviderMigrator alloc] initWithUserDefaults:user_defaults_
                                                           key:store_key_
                                                 passwordStore:mock_store_
                                                  passkeyStore:nil];
  EXPECT_TRUE(migrator);

  // Start migration.
  PasswordForm expected = PasswordFormFromCredential(credential);
  EXPECT_CALL(*mock_store_, AddLogin(expected, _));
  __block BOOL blockWaitCompleted = false;
  [migrator startMigrationWithCompletion:^(BOOL success, NSError* error) {
    EXPECT_TRUE(success);
    EXPECT_FALSE(error);
    blockWaitCompleted = true;
  }];
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForFileOperationTimeout, ^bool {
    return blockWaitCompleted;
  }));

  // Reload temp store.
  store =
      [[UserDefaultsCredentialStore alloc] initWithUserDefaults:user_defaults_
                                                            key:store_key_];
  // Verify credentials are empty
  EXPECT_EQ(store.credentials.count, 0u);
}

// Tests basic migration for 1 passkey credential.
TEST_F(CredentialProviderMigratorTest, PasskeyMigration) {
  // Create temp store and add 1 credential.
  UserDefaultsCredentialStore* store =
      [[UserDefaultsCredentialStore alloc] initWithUserDefaults:user_defaults_
                                                            key:store_key_];
  id<Credential> credential = TestPasskeyCredential();
  [store addCredential:credential];
  [store saveDataWithCompletion:^(NSError* error) {
    EXPECT_TRUE(error == nil)
        << SysNSStringToUTF8([error localizedDescription]);
  }];
  EXPECT_EQ(store.credentials.count, 1u);

  // Create the migrator to be tested.
  CredentialProviderMigrator* migrator = [[CredentialProviderMigrator alloc]
      initWithUserDefaults:user_defaults_
                       key:store_key_
             passwordStore:mock_store_
              passkeyStore:&test_passkey_model_];
  EXPECT_TRUE(migrator);

  // Start migration.
  sync_pb::WebauthnCredentialSpecifics expected =
      PasskeyFromCredential(credential);
  __block BOOL blockWaitCompleted = false;
  [migrator startMigrationWithCompletion:^(BOOL success, NSError* error) {
    EXPECT_TRUE(success);
    EXPECT_FALSE(error);
    blockWaitCompleted = true;
  }];
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForFileOperationTimeout, ^bool {
    return blockWaitCompleted;
  }));

  // Reload temp store.
  store =
      [[UserDefaultsCredentialStore alloc] initWithUserDefaults:user_defaults_
                                                            key:store_key_];
  // Verify credentials are empty
  EXPECT_EQ(store.credentials.count, 0u);

  // Verify that the credential is migrated.
  std::vector<sync_pb::WebauthnCredentialSpecifics> passkeys =
      test_passkey_model_.GetAllPasskeys();
  EXPECT_EQ(passkeys.size(), 1u);
  EXPECT_EQ(passkeys[0].sync_id(), expected.sync_id());
  EXPECT_EQ(passkeys[0].credential_id(), expected.credential_id());
  EXPECT_EQ(passkeys[0].rp_id(), expected.rp_id());
  EXPECT_EQ(passkeys[0].user_id(), expected.user_id());
  EXPECT_EQ(passkeys[0].user_name(), expected.user_name());
  EXPECT_EQ(passkeys[0].user_display_name(), expected.user_display_name());
  EXPECT_EQ(passkeys[0].creation_time(), expected.creation_time());
  EXPECT_EQ(passkeys[0].last_used_time_windows_epoch_micros(),
            expected.last_used_time_windows_epoch_micros());
}

}  // namespace