chromium/chrome/browser/password_check/android/password_check_manager_unittest.cc

// 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.

#include "chrome/browser/password_check/android/password_check_manager.h"

#include <memory>
#include <optional>
#include <string_view>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "base/version.h"
#include "chrome/browser/password_check/android/password_check_ui_status.h"
#include "chrome/browser/password_manager/bulk_leak_check_service_factory.h"
#include "chrome/browser/password_manager/password_manager_test_util.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/test/base/testing_profile.h"
#include "components/password_manager/core/browser/leak_detection/bulk_leak_check_service.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/password_manager/core/browser/password_manager_test_utils.h"
#include "components/password_manager/core/browser/password_store/test_password_store.h"
#include "components/password_manager/core/browser/ui/insecure_credentials_manager.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
#include "components/prefs/testing_pref_service.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "components/sync/test/test_sync_service.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/browser/browser_context.h"
#include "content/public/test/browser_task_environment.h"
#include "services/network/test/test_shared_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using password_manager::BulkLeakCheckService;
using password_manager::InsecureType;
using password_manager::PasswordCheckUIStatus;
using password_manager::PasswordForm;
using password_manager::TestPasswordStore;
using password_manager::prefs::kLastTimePasswordCheckCompleted;
using testing::_;
using testing::AtLeast;
using testing::Each;
using testing::ElementsAre;
using testing::Field;
using testing::Invoke;
using testing::IsEmpty;
using testing::Key;
using testing::NiceMock;
using testing::Property;
using testing::Return;

using CompromisedCredentialForUI =
    PasswordCheckManager::CompromisedCredentialForUI;
using State = password_manager::BulkLeakCheckService::State;

namespace {

constexpr char kExampleCom[] = "https://example.com";
constexpr char kExampleOrg[] = "http://www.example.org";
constexpr char kExampleApp[] = "com.example.app";

constexpr char16_t kUsername1[] = u"alice";
constexpr char16_t kUsername2[] = u"bob";

constexpr char16_t kPassword1[] = u"s3cre3t";

constexpr char kTestEmail[] = "[email protected]";

MATCHER_P(MatchInsecureType,
          insecure_type,
          base::StrCat({negation ? "does not " : "", "match the type flag"})) {
  return arg.contains(insecure_type);
}

class MockPasswordCheckManagerObserver : public PasswordCheckManager::Observer {
 public:
  MOCK_METHOD(void, OnSavedPasswordsFetched, (int), (override));

  MOCK_METHOD(void, OnCompromisedCredentialsChanged, (int), (override));

  MOCK_METHOD(void,
              OnPasswordCheckStatusChanged,
              (password_manager::PasswordCheckUIStatus),
              (override));

  MOCK_METHOD(void, OnPasswordCheckProgressChanged, (int, int), (override));
};

BulkLeakCheckService* CreateAndUseBulkLeakCheckService(
    signin::IdentityManager* identity_manager,
    Profile* profile) {
  return BulkLeakCheckServiceFactory::GetInstance()
      ->SetTestingSubclassFactoryAndUse(
          profile, base::BindOnce(
                       [](signin::IdentityManager* identity_manager,
                          content::BrowserContext*) {
                         return std::make_unique<BulkLeakCheckService>(
                             identity_manager,
                             base::MakeRefCounted<
                                 network::TestSharedURLLoaderFactory>());
                       },
                       base::Unretained(identity_manager)));
}

syncer::TestSyncService* CreateAndUseSyncService(Profile* profile) {
  return SyncServiceFactory::GetInstance()->SetTestingSubclassFactoryAndUse(
      profile, base::BindOnce([](content::BrowserContext*) {
        return std::make_unique<syncer::TestSyncService>();
      }));
}

PasswordForm MakeSavedPassword(std::string_view signon_realm,
                               std::u16string_view username,
                               std::u16string_view password = kPassword1,
                               std::u16string_view username_element = u"") {
  PasswordForm form;
  form.signon_realm = std::string(signon_realm);
  form.url = GURL(signon_realm);
  form.username_value = std::u16string(username);
  form.password_value = std::u16string(password);
  form.username_element = std::u16string(username_element);
  return form;
}

std::string MakeAndroidRealm(std::string_view package_name) {
  return base::StrCat({"android://hash@", package_name});
}
PasswordForm MakeSavedAndroidPassword(
    std::string_view package_name,
    std::u16string_view username,
    std::string_view app_display_name = "",
    std::string_view affiliated_web_realm = "") {
  PasswordForm form;
  form.signon_realm = MakeAndroidRealm(package_name);
  form.username_value = std::u16string(username);
  form.app_display_name = std::string(app_display_name);
  form.affiliated_web_realm = std::string(affiliated_web_realm);
  return form;
}

void AddIssueToForm(PasswordForm* form,
                    InsecureType type = InsecureType::kLeaked,
                    base::TimeDelta time_since_creation = base::TimeDelta()) {
  form->password_issues.insert_or_assign(
      type, password_manager::InsecurityMetadata(
                base::Time::Now() - time_since_creation,
                password_manager::IsMuted(false),
                password_manager::TriggerBackendNotification(false)));
}

// Creates matcher for a given compromised credential
auto ExpectCompromisedCredentialForUI(
    const std::u16string& display_username,
    const std::u16string& display_origin,
    const GURL& url,
    const std::optional<std::string>& package_name,
    const std::optional<std::string>& change_password_url,
    InsecureType insecure_type) {
  auto package_name_field_matcher =
      package_name.has_value()
          ? Field(&CompromisedCredentialForUI::package_name,
                  package_name.value())
          : Field(&CompromisedCredentialForUI::package_name, IsEmpty());
  auto change_password_url_field_matcher =
      change_password_url.has_value()
          ? Field(&CompromisedCredentialForUI::change_password_url,
                  change_password_url.value())
          : Field(&CompromisedCredentialForUI::change_password_url, IsEmpty());
  return AllOf(
      Field(&CompromisedCredentialForUI::display_username, display_username),
      Field(&CompromisedCredentialForUI::display_origin, display_origin),
      Property(&CompromisedCredentialForUI::GetURL, url),
      package_name_field_matcher, change_password_url_field_matcher,
      Field(&CompromisedCredentialForUI::password_issues,
            MatchInsecureType(insecure_type)),
      Property(&CompromisedCredentialForUI::IsLeaked,
               insecure_type == InsecureType::kLeaked),
      Property(&CompromisedCredentialForUI::IsPhished,
               insecure_type == InsecureType::kPhished));
}

}  // namespace

class PasswordCheckManagerTest : public testing::Test {
 public:
  void InitializeManager() {
    manager_ =
        std::make_unique<PasswordCheckManager>(&profile_, &mock_observer_);
  }

  void RunUntilIdle() { task_env_.RunUntilIdle(); }

  signin::IdentityTestEnvironment& identity_test_env() {
    return identity_test_env_;
  }
  BulkLeakCheckService* service() { return service_; }
  TestPasswordStore& store() { return *store_; }
  MockPasswordCheckManagerObserver& mock_observer() { return mock_observer_; }
  PasswordCheckManager& manager() { return *manager_; }
  base::test::ScopedFeatureList& feature_list() { return feature_list_; }
  syncer::TestSyncService& sync_service() { return *sync_service_; }

 private:
  content::BrowserTaskEnvironment task_env_;
  signin::IdentityTestEnvironment identity_test_env_;
  TestingProfile profile_;
  raw_ptr<BulkLeakCheckService> service_ =
      CreateAndUseBulkLeakCheckService(identity_test_env_.identity_manager(),
                                       &profile_);
  scoped_refptr<TestPasswordStore> store_ =
      CreateAndUseTestPasswordStore(&profile_);
  raw_ptr<syncer::TestSyncService> sync_service_ =
      CreateAndUseSyncService(&profile_);
  NiceMock<MockPasswordCheckManagerObserver> mock_observer_;
  base::test::ScopedFeatureList feature_list_;
  std::unique_ptr<PasswordCheckManager> manager_;
};

TEST_F(PasswordCheckManagerTest, SendsNoPasswordsMessageIfNoPasswordsAreSaved) {
  EXPECT_CALL(mock_observer(), OnPasswordCheckStatusChanged(
                                   PasswordCheckUIStatus::kErrorNoPasswords));
  InitializeManager();
  RunUntilIdle();
}

TEST_F(PasswordCheckManagerTest, OnSavedPasswordsFetched) {
  store().AddLogin(MakeSavedPassword(kExampleCom, kUsername1));
  EXPECT_CALL(mock_observer(), OnSavedPasswordsFetched(1));
  InitializeManager();
  RunUntilIdle();

  // Verify that OnSavedPasswordsFetched is not called after the initial fetch
  // even if the saved passwords change.
  EXPECT_CALL(mock_observer(), OnSavedPasswordsFetched(_)).Times(0);
  store().AddLogin(MakeSavedPassword(kExampleCom, kUsername2));
  RunUntilIdle();
}

TEST_F(PasswordCheckManagerTest, OnCompromisedCredentialsChanged) {
  // This is called on multiple events: once for saved passwords retrieval,
  // and once when the saved password is added.
  EXPECT_CALL(mock_observer(), OnCompromisedCredentialsChanged(0)).Times(2);
  InitializeManager();
  PasswordForm form = MakeSavedPassword(kExampleCom, kUsername1);
  store().AddLogin(form);
  RunUntilIdle();

  EXPECT_CALL(mock_observer(), OnCompromisedCredentialsChanged(1));
  AddIssueToForm(&form);
  store().UpdateLogin(form);
  RunUntilIdle();
}

TEST_F(PasswordCheckManagerTest, RunCheckAfterLastInitialization) {
  identity_test_env().MakePrimaryAccountAvailable(
      kTestEmail, signin::ConsentLevel::kSignin);
  EXPECT_CALL(mock_observer(), OnPasswordCheckStatusChanged(_))
      .Times(AtLeast(1));
  EXPECT_CALL(mock_observer(), OnSavedPasswordsFetched(1));
  store().AddLogin(MakeSavedPassword(kExampleCom, kUsername1));
  InitializeManager();

  // Initialization is incomplete, so check shouldn't run.
  manager().StartCheck();  // Try to start a check — has no immediate effect.
  service()->set_state_and_notify(State::kIdle);
  // Since check hasn't started, the last completion time should remain 0.
  EXPECT_EQ(0.0, manager().GetLastCheckTimestamp().InSecondsFSinceUnixEpoch());

  // Complete pending initialization. The check should run now.
  EXPECT_CALL(mock_observer(), OnCompromisedCredentialsChanged(0))
      .Times(AtLeast(1));
  RunUntilIdle();
  service()->set_state_and_notify(State::kIdle);  // Complete check, if any.
  // Check should have started and the last completion time be non-zero.
  EXPECT_NE(0.0, manager().GetLastCheckTimestamp().InSecondsFSinceUnixEpoch());
}

TEST_F(PasswordCheckManagerTest, CorrectlyCreatesUIStructForSiteCredential) {
  InitializeManager();
  PasswordForm form = MakeSavedPassword(kExampleCom, kUsername1);
  AddIssueToForm(&form);
  store().AddLogin(form);
  RunUntilIdle();
  EXPECT_THAT(manager().GetCompromisedCredentials(),
              ElementsAre(ExpectCompromisedCredentialForUI(
                  kUsername1, u"example.com", GURL(kExampleCom), std::nullopt,
                  "https://example.com/.well-known/change-password",
                  InsecureType::kLeaked)));
}

TEST_F(PasswordCheckManagerTest, CorrectlyCreatesUIStructForAppCredentials) {
  InitializeManager();

  // A credential without affiliation information.
  PasswordForm form_no_affiliation =
      MakeSavedAndroidPassword(kExampleApp, kUsername1);
  AddIssueToForm(&form_no_affiliation);
  store().AddLogin(form_no_affiliation);

  // A credential for which affiliation information is known.
  PasswordForm form_with_affiliation = MakeSavedAndroidPassword(
      kExampleApp, kUsername2, "Example App", kExampleCom);
  AddIssueToForm(&form_with_affiliation);
  store().AddLogin(form_with_affiliation);
  RunUntilIdle();

  // Some weak, reused and secure credentials that should be ignored.
  PasswordForm form_weak = MakeSavedAndroidPassword(kExampleOrg, kUsername1);
  AddIssueToForm(&form_weak, InsecureType::kWeak);
  store().AddLogin(form_weak);
  PasswordForm form_reused = MakeSavedAndroidPassword(kExampleCom, kUsername2);
  AddIssueToForm(&form_reused, InsecureType::kReused);
  store().AddLogin(form_reused);
  store().AddLogin(MakeSavedAndroidPassword(kExampleOrg, kUsername2));

  EXPECT_THAT(manager().GetCompromisedCredentialsCount(), 2);
  EXPECT_THAT(manager().GetCompromisedCredentials(),
              UnorderedElementsAre(
                  ExpectCompromisedCredentialForUI(
                      kUsername1, u"App (com.example.app)", GURL(),
                      "com.example.app", std::nullopt, InsecureType::kLeaked),
                  ExpectCompromisedCredentialForUI(
                      kUsername2, u"Example App", GURL(kExampleCom),
                      "com.example.app", std::nullopt, InsecureType::kLeaked)));
}

TEST_F(PasswordCheckManagerTest, SetsTimestampOnSuccessfulCheck) {
  identity_test_env().MakePrimaryAccountAvailable(
      kTestEmail, signin::ConsentLevel::kSignin);
  InitializeManager();
  store().AddLogin(MakeSavedPassword(kExampleCom, kUsername1));
  RunUntilIdle();

  // Pretend to start the check so that the manager thinks a check is running.
  manager().StartCheck();

  // Change the state to idle to simulate a successful check finish.
  service()->set_state_and_notify(State::kIdle);
  EXPECT_NE(0.0, manager().GetLastCheckTimestamp().InSecondsFSinceUnixEpoch());
}

TEST_F(PasswordCheckManagerTest, DoesntRecordTimestampOfUnsuccessfulCheck) {
  identity_test_env().MakeAccountAvailable(kTestEmail);
  InitializeManager();
  store().AddLogin(MakeSavedPassword(kExampleCom, kUsername1));
  RunUntilIdle();

  // Pretend to start the check so that the manager thinks a check is running.
  manager().StartCheck();

  // Change the state to an error state to simulate a unsuccessful check finish.
  service()->set_state_and_notify(State::kSignedOut);
  EXPECT_EQ(0.0, manager().GetLastCheckTimestamp().InSecondsFSinceUnixEpoch());
}

TEST_F(PasswordCheckManagerTest, CorrectlyCreatesUIStruct) {
  InitializeManager();
  // Disable password sync
  sync_service().GetUserSettings()->SetSelectedTypes(
      /*sync_everything=*/false,
      /*types=*/syncer::UserSelectableTypeSet());
  PasswordForm form = MakeSavedPassword(kExampleCom, kUsername1);
  AddIssueToForm(&form);
  store().AddLogin(form);
  RunUntilIdle();

  EXPECT_THAT(manager().GetCompromisedCredentials(),
              ElementsAre(ExpectCompromisedCredentialForUI(
                  kUsername1, u"example.com", GURL(kExampleCom), std::nullopt,
                  "https://example.com/.well-known/change-password",
                  InsecureType::kLeaked)));
}

TEST_F(PasswordCheckManagerTest, UpdatesProgressCorrectly) {
  identity_test_env().MakePrimaryAccountAvailable(
      kTestEmail, signin::ConsentLevel::kSignin);
  InitializeManager();

  store().AddLogin(MakeSavedPassword(kExampleCom, kUsername1, kPassword1));
  store().AddLogin(MakeSavedPassword(kExampleOrg, kUsername1, kPassword1));
  store().AddLogin(MakeSavedPassword(kExampleCom, kUsername2));
  RunUntilIdle();

  EXPECT_CALL(mock_observer(), OnPasswordCheckProgressChanged(0, 3));
  manager().StartCheck();

  // Expect that 2 credentials were processed, even if there is only one
  // reply, because of the deduplication logic.
  EXPECT_CALL(mock_observer(), OnPasswordCheckProgressChanged(2, 1));
  static_cast<password_manager::BulkLeakCheckDelegateInterface*>(service())
      ->OnFinishedCredential(
          password_manager::LeakCheckCredential(kUsername1, kPassword1),
          password_manager::IsLeaked(false));
}

TEST_F(PasswordCheckManagerTest, DoesntUpdateNonExistingProgress) {
  InitializeManager();
  store().AddLogin(MakeSavedPassword(kExampleCom, kUsername1, kPassword1));
  store().AddLogin(MakeSavedPassword(kExampleOrg, kUsername1, kPassword1));
  store().AddLogin(MakeSavedPassword(kExampleCom, kUsername2));
  RunUntilIdle();

  EXPECT_CALL(mock_observer(), OnPasswordCheckProgressChanged).Times(0);
  static_cast<password_manager::BulkLeakCheckDelegateInterface*>(service())
      ->OnFinishedCredential(
          password_manager::LeakCheckCredential(kUsername1, kPassword1),
          password_manager::IsLeaked(false));
}

TEST_F(PasswordCheckManagerTest, TurnsIdleIntoNoPasswords) {
  InitializeManager();
  RunUntilIdle();

  EXPECT_CALL(mock_observer(),
              OnPasswordCheckStatusChanged(PasswordCheckUIStatus::kRunning))
      .Times(1);
  EXPECT_CALL(mock_observer(),
              OnPasswordCheckStatusChanged(PasswordCheckUIStatus::kIdle))
      .Times(0);
  EXPECT_CALL(mock_observer(), OnPasswordCheckStatusChanged(
                                   PasswordCheckUIStatus::kErrorNoPasswords))
      .Times(1);

  manager().StartCheck();
}