chromium/ios/chrome/browser/safe_browsing/model/chrome_password_protection_service_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/safe_browsing/model/chrome_password_protection_service.h"

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

#import "base/memory/raw_ptr.h"
#import "base/memory/scoped_refptr.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/mock_callback.h"
#import "base/values.h"
#import "components/keyed_service/core/service_access_type.h"
#import "components/password_manager/core/browser/password_manager_metrics_util.h"
#import "components/password_manager/core/browser/password_manager_test_utils.h"
#import "components/password_manager/core/browser/password_reuse_detector.h"
#import "components/password_manager/core/browser/password_store/mock_password_store_interface.h"
#import "components/prefs/pref_service.h"
#import "components/safe_browsing/core/browser/password_protection/metrics_util.h"
#import "components/safe_browsing/core/common/safe_browsing_prefs.h"
#import "components/signin/public/identity_manager/account_info.h"
#import "components/signin/public/identity_manager/identity_test_environment.h"
#import "components/strings/grit/components_strings.h"
#import "components/sync/protocol/gaia_password_reuse.pb.h"
#import "components/sync_user_events/fake_user_event_service.h"
#import "ios/chrome/browser/history/model/history_service_factory.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_profile_password_store_factory.h"
#import "ios/chrome/browser/safe_browsing/model/safe_browsing_metrics_collector_factory.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/sync/model/ios_user_event_service_factory.h"
#import "ios/components/security_interstitials/safe_browsing/fake_safe_browsing_service.h"
#import "ios/web/public/navigation/referrer.h"
#import "ios/web/public/test/fakes/fake_navigation_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/test/web_task_environment.h"
#import "ios/web/public/web_state.h"
#import "testing/gmock/include/gmock/gmock.h"
#import "testing/platform_test.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/page_transition_types.h"
#import "url/gurl.h"

using password_manager::metrics_util::PasswordType;
using safe_browsing::LoginReputationClientRequest;
using safe_browsing::LoginReputationClientResponse;
using safe_browsing::PasswordProtectionTrigger;
using safe_browsing::RequestOutcome;
using safe_browsing::ReusedPasswordAccountType;
using sync_pb::GaiaPasswordReuse;
using ::testing::_;
using PasswordReuseDialogInteraction =
    sync_pb::GaiaPasswordReuse::PasswordReuseDialogInteraction;
using PasswordReuseLookup = sync_pb::GaiaPasswordReuse::PasswordReuseLookup;

namespace {

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

const unsigned int kMinute = 60;
const unsigned int kDay = 24 * 60 * kMinute;

constexpr struct {
  // The response from the password protection service.
  RequestOutcome request_outcome;
  // The enum to log in the user event for that response.
  PasswordReuseLookup::LookupResult lookup_result;
} kTestCasesWithoutVerdict[]{
    {RequestOutcome::MATCHED_ALLOWLIST, PasswordReuseLookup::ALLOWLIST_HIT},
    {RequestOutcome::URL_NOT_VALID_FOR_REPUTATION_COMPUTING,
     PasswordReuseLookup::URL_UNSUPPORTED},
    {RequestOutcome::CANCELED, PasswordReuseLookup::REQUEST_FAILURE},
    {RequestOutcome::TIMEDOUT, PasswordReuseLookup::REQUEST_FAILURE},
    {RequestOutcome::DISABLED_DUE_TO_INCOGNITO,
     PasswordReuseLookup::REQUEST_FAILURE},
    {RequestOutcome::REQUEST_MALFORMED, PasswordReuseLookup::REQUEST_FAILURE},
    {RequestOutcome::FETCH_FAILED, PasswordReuseLookup::REQUEST_FAILURE},
    {RequestOutcome::RESPONSE_MALFORMED, PasswordReuseLookup::REQUEST_FAILURE},
    {RequestOutcome::SERVICE_DESTROYED, PasswordReuseLookup::REQUEST_FAILURE},
    {RequestOutcome::DISABLED_DUE_TO_FEATURE_DISABLED,
     PasswordReuseLookup::REQUEST_FAILURE},
    {RequestOutcome::DISABLED_DUE_TO_USER_POPULATION,
     PasswordReuseLookup::REQUEST_FAILURE}};

// A test factory to create a FakeUserEventService.
std::unique_ptr<KeyedService> CreateFakeUserEventService(
    web::BrowserState* browser_state) {
  return std::make_unique<syncer::FakeUserEventService>();
}
}  // namespace

class FakeChromePasswordProtectionService
    : public ChromePasswordProtectionService {
 public:
  explicit FakeChromePasswordProtectionService(
      SafeBrowsingService* sb_service,
      ChromeBrowserState* browser_state,
      history::HistoryService* history_service,
      safe_browsing::SafeBrowsingMetricsCollector*
          safe_browsing_metrics_collector,
      ChangePhishedCredentialsCallback add_phished_credentials,
      ChangePhishedCredentialsCallback remove_phished_credentials)
      : ChromePasswordProtectionService(sb_service,
                                        browser_state,
                                        history_service,
                                        safe_browsing_metrics_collector,
                                        add_phished_credentials,
                                        remove_phished_credentials),
        is_incognito_(false),
        is_account_signed_in_(false),
        is_no_hosted_domain_found_(false) {}

  bool IsIncognito() override { return is_incognito_; }
  bool IsPrimaryAccountSignedIn() const override {
    return is_account_signed_in_;
  }
  bool IsAccountGmail(const std::string& username) const override {
    return is_no_hosted_domain_found_;
  }
  void SetIsIncognito(bool is_incognito) { is_incognito_ = is_incognito; }
  void SetIsAccountSignedIn(bool is_account_signed_in) {
    is_account_signed_in_ = is_account_signed_in;
  }
  void SetIsNoHostedDomainFound(bool is_no_hosted_domain_found) {
    is_no_hosted_domain_found_ = is_no_hosted_domain_found;
  }

 protected:
  friend class ChromePasswordProtectionServiceTest;

 private:
  bool is_incognito_;
  bool is_account_signed_in_;
  bool is_no_hosted_domain_found_;
};

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

  void SetUp() override {
    PlatformTest::SetUp();

    TestChromeBrowserState::Builder builder;
    builder.AddTestingFactory(
        IOSChromeProfilePasswordStoreFactory::GetInstance(),
        base::BindRepeating(&password_manager::BuildPasswordStoreInterface<
                            web::BrowserState,
                            password_manager::MockPasswordStoreInterface>));
    builder.AddTestingFactory(IOSUserEventServiceFactory::GetInstance(),
                              base::BindRepeating(&CreateFakeUserEventService));
    browser_state_ = std::move(builder).Build();

    web::WebState::CreateParams params(browser_state_.get());
    web_state_ = web::WebState::Create(params);
    web_state_->GetView();
    web_state_->SetKeepRenderProcessAlive(true);

    safe_browsing_service_ = base::MakeRefCounted<FakeSafeBrowsingService>();

    service_ = std::make_unique<FakeChromePasswordProtectionService>(
        safe_browsing_service_.get(), browser_state_.get(),
        ios::HistoryServiceFactory::GetForBrowserState(
            browser_state_.get(), ServiceAccessType::EXPLICIT_ACCESS),
        SafeBrowsingMetricsCollectorFactory::GetForBrowserState(
            browser_state_.get()),
        mock_add_callback_.Get(), mock_remove_callback_.Get());

    auto navigation_manager = std::make_unique<web::FakeNavigationManager>();
    fake_navigation_manager_ = navigation_manager.get();
    fake_web_state_.SetNavigationManager(std::move(navigation_manager));
    fake_web_state_.SetBrowserState(browser_state_.get());
  }

  void NavigateAndCommit(const GURL& url) {
    fake_navigation_manager_->AddItem(
        url, ui::PageTransition::PAGE_TRANSITION_TYPED);
    web::NavigationItem* item = fake_navigation_manager_->GetItemAtIndex(
        fake_navigation_manager_->GetItemCount() - 1);
    item->SetTimestamp(base::Time::Now());
    fake_navigation_manager_->SetLastCommittedItem(item);
  }

  syncer::FakeUserEventService* GetUserEventService() {
    return static_cast<syncer::FakeUserEventService*>(
        IOSUserEventServiceFactory::GetForBrowserState(browser_state_.get()));
  }

  CoreAccountInfo SetPrimaryAccount(const std::string& email) {
    identity_test_env_.MakeAccountAvailable(email);
    return identity_test_env_.SetPrimaryAccount(email,
                                                signin::ConsentLevel::kSignin);
  }

  void SetUpSyncAccount(const std::string& hosted_domain,
                        const CoreAccountInfo& account_info) {
    identity_test_env_.SimulateSuccessfulFetchOfAccountInfo(
        account_info.account_id, account_info.email, account_info.gaia,
        hosted_domain, "full_name", "given_name", "locale",
        "http://picture.example.com/picture.jpg");
  }

  LoginReputationClientResponse CreateVerdictProto(
      LoginReputationClientResponse::VerdictType verdict,
      int cache_duration_sec,
      const std::string& cache_expression) {
    LoginReputationClientResponse verdict_proto;
    verdict_proto.set_verdict_type(verdict);
    verdict_proto.set_cache_duration_sec(cache_duration_sec);
    verdict_proto.set_cache_expression(cache_expression);
    return verdict_proto;
  }

  void CacheVerdict(const GURL& url,
                    LoginReputationClientRequest::TriggerType trigger,
                    ReusedPasswordAccountType password_type,
                    LoginReputationClientResponse::VerdictType verdict,
                    int cache_duration_sec,
                    const std::string& cache_expression,
                    const base::Time& verdict_received_time) {
    ASSERT_FALSE(cache_expression.empty());
    LoginReputationClientResponse response(
        CreateVerdictProto(verdict, cache_duration_sec, cache_expression));
    service_->CacheVerdict(url, trigger, password_type, response,
                           verdict_received_time);
  }

  size_t GetStoredVerdictCount(LoginReputationClientRequest::TriggerType type) {
    return service_->GetStoredVerdictCount(type);
  }

 protected:
  web::WebState* web_state() { return web_state_.get(); }

  web::WebTaskEnvironment task_environment_;
  std::unique_ptr<TestChromeBrowserState> browser_state_;
  std::unique_ptr<web::WebState> web_state_;

  scoped_refptr<SafeBrowsingService> safe_browsing_service_;
  std::unique_ptr<FakeChromePasswordProtectionService> service_;
  web::FakeWebState fake_web_state_;
  raw_ptr<web::FakeNavigationManager> fake_navigation_manager_;
  base::MockCallback<
      ChromePasswordProtectionService::ChangePhishedCredentialsCallback>
      mock_add_callback_;
  base::MockCallback<
      ChromePasswordProtectionService::ChangePhishedCredentialsCallback>
      mock_remove_callback_;
  signin::IdentityTestEnvironment identity_test_env_;
};

// All pinging is disabled when safe browsing is disabled.
TEST_F(ChromePasswordProtectionServiceTest,
       VerifyPingingDisabledWhenSafeBrowsingDisabled) {
  browser_state_->GetPrefs()->SetBoolean(prefs::kSafeBrowsingEnabled, false);

  LoginReputationClientRequest::TriggerType trigger_type;
  ReusedPasswordAccountType reused_password_type;

  trigger_type = LoginReputationClientRequest::PASSWORD_REUSE_EVENT;
  reused_password_type.set_account_type(
      ReusedPasswordAccountType::SAVED_PASSWORD);
  service_->SetIsIncognito(false);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));
  service_->SetIsIncognito(true);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));

  trigger_type = LoginReputationClientRequest::PASSWORD_REUSE_EVENT;
  reused_password_type.set_account_type(ReusedPasswordAccountType::UNKNOWN);
  service_->SetIsIncognito(false);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));

  trigger_type = LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE;
  reused_password_type.set_account_type(ReusedPasswordAccountType::UNKNOWN);
  service_->SetIsIncognito(false);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));
  service_->SetIsIncognito(true);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));

  reused_password_type.set_account_type(ReusedPasswordAccountType::GMAIL);
  reused_password_type.set_is_account_syncing(true);
  trigger_type = LoginReputationClientRequest::PASSWORD_REUSE_EVENT;
  service_->SetIsIncognito(false);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));
  service_->SetIsIncognito(true);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));
  service_->SetIsIncognito(true);
  service_->SetIsNoHostedDomainFound(true);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));
  browser_state_->GetPrefs()->SetInteger(
      prefs::kPasswordProtectionWarningTrigger, safe_browsing::PASSWORD_REUSE);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));
}

// Saved password pinging is enabled.
TEST_F(ChromePasswordProtectionServiceTest,
       VerifyUserPopulationForSavedPasswordEntryPing) {
  LoginReputationClientRequest::TriggerType trigger_type =
      LoginReputationClientRequest::PASSWORD_REUSE_EVENT;
  ReusedPasswordAccountType reused_password_type;
  reused_password_type.set_account_type(
      ReusedPasswordAccountType::SAVED_PASSWORD);

  service_->SetIsIncognito(false);
  EXPECT_TRUE(service_->IsPingingEnabled(trigger_type, reused_password_type));

  service_->SetIsIncognito(true);
  EXPECT_TRUE(service_->IsPingingEnabled(trigger_type, reused_password_type));

  service_->SetIsIncognito(false);
  reused_password_type.set_account_type(ReusedPasswordAccountType::UNKNOWN);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));
}

// Password field on focus pinging is disabled on iOS since SBER and enhanced
// protection are both disabled.
TEST_F(ChromePasswordProtectionServiceTest,
       VerifyUserPopulationForPasswordOnFocusPing) {
  LoginReputationClientRequest::TriggerType trigger_type =
      LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE;
  ReusedPasswordAccountType reused_password_type;
  reused_password_type.set_account_type(ReusedPasswordAccountType::UNKNOWN);

  service_->SetIsIncognito(false);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));

  service_->SetIsIncognito(true);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));
}

// Sync password entry pinging is not yet enabled for iOS.
TEST_F(ChromePasswordProtectionServiceTest,
       VerifyUserPopulationForSyncPasswordEntryPing) {
  LoginReputationClientRequest::TriggerType trigger_type =
      LoginReputationClientRequest::PASSWORD_REUSE_EVENT;
  // Sets up the account as a gmail account as there is no hosted domain.
  ReusedPasswordAccountType reused_password_type;
  reused_password_type.set_account_type(ReusedPasswordAccountType::GMAIL);
  reused_password_type.set_is_account_syncing(true);

  service_->SetIsIncognito(false);
  EXPECT_FALSE(service_->IsPingingEnabled(
      LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
      reused_password_type));

  service_->SetIsIncognito(true);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));

  service_->SetIsIncognito(true);
  service_->SetIsNoHostedDomainFound(true);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));

  browser_state_->GetPrefs()->SetInteger(
      prefs::kPasswordProtectionWarningTrigger,
      safe_browsing::PASSWORD_PROTECTION_OFF);
  service_->SetIsIncognito(false);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));

  browser_state_->GetPrefs()->SetInteger(
      prefs::kPasswordProtectionWarningTrigger, safe_browsing::PASSWORD_REUSE);
  EXPECT_FALSE(service_->IsPingingEnabled(trigger_type, reused_password_type));
}

TEST_F(ChromePasswordProtectionServiceTest,
       VerifyPingingIsSkippedIfMatchEnterpriseAllowlist) {
  ASSERT_FALSE(browser_state_->GetPrefs()->HasPrefPath(
      prefs::kSafeBrowsingAllowlistDomains));

  // If there's no allowlist, IsURLAllowlistedForPasswordEntry(_) should
  // return false.
  EXPECT_FALSE(service_->IsURLAllowlistedForPasswordEntry(
      GURL("https://www.mydomain.com")));

  // Verify URL is allowed after setting allowlist in prefs.
  base::Value::List allowlist;
  allowlist.Append("mydomain.com");
  allowlist.Append("mydomain.net");
  browser_state_->GetPrefs()->SetList(prefs::kSafeBrowsingAllowlistDomains,
                                      std::move(allowlist));
  EXPECT_TRUE(service_->IsURLAllowlistedForPasswordEntry(
      GURL("https://www.mydomain.com")));

  // Verify change password URL (used for enterprise) is allowed (when set in
  // prefs), even when the domain is not allowed.
  browser_state_->GetPrefs()->ClearPref(prefs::kSafeBrowsingAllowlistDomains);
  EXPECT_FALSE(service_->IsURLAllowlistedForPasswordEntry(
      GURL("https://www.mydomain.com")));
  browser_state_->GetPrefs()->SetString(
      prefs::kPasswordProtectionChangePasswordURL,
      "https://mydomain.com/change_password.html");
  EXPECT_TRUE(service_->IsURLAllowlistedForPasswordEntry(
      GURL("https://mydomain.com/change_password.html#ref?user_name=alice")));

  // Verify login URL (used for enterprise) is allowed (when set in prefs), even
  // when the domain is not allowed.
  browser_state_->GetPrefs()->ClearPref(prefs::kSafeBrowsingAllowlistDomains);
  browser_state_->GetPrefs()->ClearPref(
      prefs::kPasswordProtectionChangePasswordURL);
  EXPECT_FALSE(service_->IsURLAllowlistedForPasswordEntry(
      GURL("https://www.mydomain.com")));
  base::Value::List login_urls;
  login_urls.Append("https://mydomain.com/login.html");
  browser_state_->GetPrefs()->SetList(prefs::kPasswordProtectionLoginURLs,
                                      std::move(login_urls));
  EXPECT_TRUE(service_->IsURLAllowlistedForPasswordEntry(
      GURL("https://mydomain.com/login.html#ref?user_name=alice")));
}

TEST_F(ChromePasswordProtectionServiceTest,
       VerifyPersistPhishedSavedPasswordCredential) {
  service_->SetIsIncognito(false);
  std::vector<password_manager::MatchingReusedCredential> credentials = {
      {"http://example.test"}, {"http://2.example.com"}};

  EXPECT_CALL(mock_add_callback_, Run(_, credentials[0]));
  EXPECT_CALL(mock_add_callback_, Run(_, credentials[1]));
  service_->PersistPhishedSavedPasswordCredential(credentials);
}

TEST_F(ChromePasswordProtectionServiceTest,
       VerifyRemovePhishedSavedPasswordCredential) {
  service_->SetIsIncognito(false);
  std::vector<password_manager::MatchingReusedCredential> credentials = {
      {"http://example.test", u"username1"},
      {"http://2.example.test", u"username2"}};

  EXPECT_CALL(mock_remove_callback_, Run(_, credentials[0]));
  EXPECT_CALL(mock_remove_callback_, Run(_, credentials[1]));
  service_->RemovePhishedSavedPasswordCredential(credentials);
}

TEST_F(ChromePasswordProtectionServiceTest,
       VerifyPasswordReuseUserEventNotRecordedDueToIncognito) {
  // Configure sync account type to GMAIL.
  CoreAccountInfo account_info = SetPrimaryAccount(kTestEmail);
  SetUpSyncAccount(kNoHostedDomainFound, account_info);
  service_->SetIsIncognito(true);
  ASSERT_TRUE(service_->IsIncognito());

  // Nothing should be logged because of incognito.
  NavigateAndCommit(GURL("https:www.example.com/"));

  // PasswordReuseDetected
  service_->MaybeLogPasswordReuseDetectedEvent(web_state());
  EXPECT_TRUE(GetUserEventService()->GetRecordedUserEvents().empty());
  service_->MaybeLogPasswordReuseLookupEvent(
      web_state(), RequestOutcome::MATCHED_ALLOWLIST,
      PasswordType::PRIMARY_ACCOUNT_PASSWORD, nullptr);
  EXPECT_TRUE(GetUserEventService()->GetRecordedUserEvents().empty());

  // PasswordReuseLookup
  unsigned long t = 0;
  for (const auto& it : kTestCasesWithoutVerdict) {
    service_->MaybeLogPasswordReuseLookupEvent(
        web_state(), it.request_outcome, PasswordType::PRIMARY_ACCOUNT_PASSWORD,
        nullptr);
    ASSERT_TRUE(GetUserEventService()->GetRecordedUserEvents().empty()) << t;
    t++;
  }

  // PasswordReuseDialogInteraction
  service_->MaybeLogPasswordReuseDialogInteraction(
      1000 /* navigation_id */,
      PasswordReuseDialogInteraction::WARNING_ACTION_TAKEN);
  ASSERT_TRUE(GetUserEventService()->GetRecordedUserEvents().empty());
}

TEST_F(ChromePasswordProtectionServiceTest,
       VerifyPasswordReuseDetectedUserEventRecorded) {
  // Configure sync account type to GMAIL.
  CoreAccountInfo account_info = SetPrimaryAccount(kTestEmail);
  SetUpSyncAccount(kNoHostedDomainFound, account_info);
  service_->SetIsAccountSignedIn(true);
  NavigateAndCommit(GURL("https://www.example.com/"));

  // Case 1: safe_browsing_enabled = true
  browser_state_->GetPrefs()->SetBoolean(prefs::kSafeBrowsingEnabled, true);
  service_->MaybeLogPasswordReuseDetectedEvent(&fake_web_state_);
  ASSERT_EQ(1ul, GetUserEventService()->GetRecordedUserEvents().size());
  GaiaPasswordReuse event = GetUserEventService()
                                ->GetRecordedUserEvents()[0]
                                .gaia_password_reuse_event();
  EXPECT_TRUE(event.reuse_detected().status().enabled());

  // Case 2: safe_browsing_enabled = false
  browser_state_->GetPrefs()->SetBoolean(prefs::kSafeBrowsingEnabled, false);
  service_->MaybeLogPasswordReuseDetectedEvent(&fake_web_state_);
  ASSERT_EQ(2ul, GetUserEventService()->GetRecordedUserEvents().size());
  event = GetUserEventService()
              ->GetRecordedUserEvents()[1]
              .gaia_password_reuse_event();
  EXPECT_FALSE(event.reuse_detected().status().enabled());
}

TEST_F(ChromePasswordProtectionServiceTest, VerifyGetWarningDetailTextSaved) {
  std::u16string warning_text =
      l10n_util::GetStringUTF16(IDS_PAGE_INFO_CHANGE_PASSWORD_DETAILS_SAVED);
  ReusedPasswordAccountType reused_password_type;
  reused_password_type.set_account_type(
      ReusedPasswordAccountType::SAVED_PASSWORD);
  EXPECT_EQ(warning_text, service_->GetWarningDetailText(reused_password_type));
}

TEST_F(ChromePasswordProtectionServiceTest, VerifySendsPingForAboutBlank) {
  ReusedPasswordAccountType reused_password_type;
  reused_password_type.set_account_type(
      ReusedPasswordAccountType::SAVED_PASSWORD);
  service_->SetIsIncognito(false);
  EXPECT_TRUE(
      service_->CanSendPing(LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                            GURL("about:blank"), reused_password_type));
}

TEST_F(ChromePasswordProtectionServiceTest, VerifyGetPingNotSentReason) {
  {
    // SBER disabled.
    ReusedPasswordAccountType reused_password_type;
    service_->SetIsIncognito(false);
    EXPECT_EQ(RequestOutcome::DISABLED_DUE_TO_USER_POPULATION,
              service_->GetPingNotSentReason(
                  LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
                  GURL("about:blank"), reused_password_type));
    reused_password_type.set_account_type(ReusedPasswordAccountType::UNKNOWN);
    EXPECT_EQ(RequestOutcome::DISABLED_DUE_TO_USER_POPULATION,
              service_->GetPingNotSentReason(
                  LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                  GURL("about:blank"), reused_password_type));
  }
  {
    // In Incognito.
    ReusedPasswordAccountType reused_password_type;
    service_->SetIsIncognito(true);
    EXPECT_EQ(RequestOutcome::DISABLED_DUE_TO_INCOGNITO,
              service_->GetPingNotSentReason(
                  LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
                  GURL("about:blank"), reused_password_type));
  }
  {
    // Turned off by admin.
    ReusedPasswordAccountType reused_password_type;
    service_->SetIsIncognito(false);
    reused_password_type.set_account_type(ReusedPasswordAccountType::GSUITE);
    browser_state_->GetPrefs()->SetInteger(
        prefs::kPasswordProtectionWarningTrigger,
        safe_browsing::PASSWORD_PROTECTION_OFF);
    EXPECT_EQ(RequestOutcome::TURNED_OFF_BY_ADMIN,
              service_->GetPingNotSentReason(
                  LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                  GURL("about:blank"), reused_password_type));
  }
  {
    // Allowlisted by policy.
    ReusedPasswordAccountType reused_password_type;
    service_->SetIsIncognito(false);
    reused_password_type.set_account_type(ReusedPasswordAccountType::GSUITE);
    browser_state_->GetPrefs()->SetInteger(
        prefs::kPasswordProtectionWarningTrigger,
        safe_browsing::PHISHING_REUSE);
    base::Value::List allowlist;
    allowlist.Append("mydomain.com");
    allowlist.Append("mydomain.net");
    browser_state_->GetPrefs()->SetList(prefs::kSafeBrowsingAllowlistDomains,
                                        std::move(allowlist));
    EXPECT_EQ(RequestOutcome::MATCHED_ENTERPRISE_ALLOWLIST,
              service_->GetPingNotSentReason(
                  LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                  GURL("https://www.mydomain.com"), reused_password_type));
  }
  {
    // Password alert mode.
    ReusedPasswordAccountType reused_password_type;
    service_->SetIsIncognito(false);
    reused_password_type.set_account_type(ReusedPasswordAccountType::UNKNOWN);
    browser_state_->GetPrefs()->SetInteger(
        prefs::kPasswordProtectionWarningTrigger,
        safe_browsing::PASSWORD_REUSE);
    EXPECT_EQ(RequestOutcome::PASSWORD_ALERT_MODE,
              service_->GetPingNotSentReason(
                  LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                  GURL("about:blank"), reused_password_type));
  }
}

TEST_F(ChromePasswordProtectionServiceTest, TestCachePasswordReuseVerdicts) {
  ASSERT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));

  service_->SetIsAccountSignedIn(true);

  // Assume each verdict has a TTL of 10 minutes.
  // Cache a verdict for http://www.test.com/foo/index.html
  ReusedPasswordAccountType reused_password_account_type;
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::GSUITE);
  reused_password_account_type.set_is_account_syncing(true);
  CacheVerdict(GURL("http://www.test.com/foo/index.html"),
               LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
               reused_password_account_type,
               LoginReputationClientResponse::SAFE, 10 * kMinute,
               "test.com/foo/", base::Time::Now());

  EXPECT_EQ(1U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));

  // Cache another verdict with the some origin and cache_expression should
  // override the cache.
  CacheVerdict(GURL("http://www.test.com/foo/index2.html"),
               LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
               reused_password_account_type,
               LoginReputationClientResponse::PHISHING, 10 * kMinute,
               "test.com/foo/", base::Time::Now());
  EXPECT_EQ(1U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
  LoginReputationClientResponse out_verdict;
  EXPECT_EQ(LoginReputationClientResponse::PHISHING,
            service_->GetCachedVerdict(
                GURL("http://www.test.com/foo/index2.html"),
                LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                reused_password_account_type, &out_verdict));

  // Cache a password reuse verdict with a different password type but same
  // origin and cache expression should add a new entry.
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::NON_GAIA_ENTERPRISE);
  CacheVerdict(GURL("http://www.test.com/foo/index2.html"),
               LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
               reused_password_account_type,
               LoginReputationClientResponse::PHISHING, 10 * kMinute,
               "test.com/foo/", base::Time::Now());
  EXPECT_EQ(2U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
  EXPECT_EQ(LoginReputationClientResponse::PHISHING,
            service_->GetCachedVerdict(
                GURL("http://www.test.com/foo/index2.html"),
                LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                reused_password_account_type, &out_verdict));

  // Cache another verdict with the same origin but different cache_expression
  // will increase the number of verdicts in the given origin.
  CacheVerdict(GURL("http://www.test.com/bar/index2.html"),
               LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
               reused_password_account_type,
               LoginReputationClientResponse::SAFE, 10 * kMinute,
               "test.com/bar/", base::Time::Now());
  EXPECT_EQ(3U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));

  // Now cache a UNFAMILIAR_LOGIN_PAGE verdict, stored verdict count for
  // PASSWORD_REUSE_EVENT should be the same.
  CacheVerdict(GURL("http://www.test.com/foobar/index3.html"),
               LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
               reused_password_account_type,
               LoginReputationClientResponse::SAFE, 10 * kMinute,
               "test.com/foobar/", base::Time::Now());
  EXPECT_EQ(3U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
  EXPECT_EQ(1U, GetStoredVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));
}

TEST_F(ChromePasswordProtectionServiceTest,
       TestCachePasswordReuseVerdictsIncognito) {
  service_->SetIsIncognito(true);
  ASSERT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));

  ReusedPasswordAccountType reused_password_account_type;
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::GSUITE);
  reused_password_account_type.set_is_account_syncing(true);
  // No verdict will be cached for incognito profile.
  CacheVerdict(GURL("http://www.test.com/foo/index.html"),
               LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
               reused_password_account_type,
               LoginReputationClientResponse::SAFE, 10 * kMinute,
               "test.com/foo/", base::Time::Now());

  EXPECT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));

  // Try cache another verdict with the some origin and cache_expression.
  // Verdict count should not increase.
  CacheVerdict(GURL("http://www.test.com/foo/index2.html"),
               LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
               reused_password_account_type,
               LoginReputationClientResponse::PHISHING, 10 * kMinute,
               "test.com/foo/", base::Time::Now());
  EXPECT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));

  // Now cache a UNFAMILIAR_LOGIN_PAGE verdict, verdict count should not
  // increase.
  CacheVerdict(GURL("http://www.test.com/foobar/index3.html"),
               LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
               reused_password_account_type,
               LoginReputationClientResponse::SAFE, 10 * kMinute,
               "test.com/foobar/", base::Time::Now());
  EXPECT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
  EXPECT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));
}

TEST_F(ChromePasswordProtectionServiceTest, TestCacheUnfamiliarLoginVerdicts) {
  ASSERT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));
  ReusedPasswordAccountType reused_password_account_type;
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::UNKNOWN);
  reused_password_account_type.set_is_account_syncing(true);
  // Assume each verdict has a TTL of 10 minutes.
  // Cache a verdict for http://www.test.com/foo/index.html
  CacheVerdict(GURL("http://www.test.com/foo/index.html"),
               LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
               reused_password_account_type,
               LoginReputationClientResponse::SAFE, 10 * kMinute,
               "test.com/foo/", base::Time::Now());

  EXPECT_EQ(1U, GetStoredVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));

  // Cache another verdict with the same origin but different cache_expression
  // will increase the number of verdicts in the given origin.
  CacheVerdict(GURL("http://www.test.com/bar/index2.html"),
               LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
               reused_password_account_type,
               LoginReputationClientResponse::SAFE, 10 * kMinute,
               "test.com/bar/", base::Time::Now());
  EXPECT_EQ(2U, GetStoredVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));

  // Now cache a PASSWORD_REUSE_EVENT verdict, stored verdict count for
  // UNFAMILIAR_LOGIN_PAGE should be the same.
  CacheVerdict(GURL("http://www.test.com/foobar/index3.html"),
               LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
               reused_password_account_type,
               LoginReputationClientResponse::SAFE, 10 * kMinute,
               "test.com/foobar/", base::Time::Now());
  EXPECT_EQ(2U, GetStoredVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));
  EXPECT_EQ(1U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
}

TEST_F(ChromePasswordProtectionServiceTest,
       TestCacheUnfamiliarLoginVerdictsIncognito) {
  service_->SetIsIncognito(true);

  ASSERT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));

  ReusedPasswordAccountType reused_password_account_type;
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::UNKNOWN);
  reused_password_account_type.set_is_account_syncing(true);
  // No verdict will be cached for incognito profile.
  CacheVerdict(GURL("http://www.test.com/foo/index.html"),
               LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
               reused_password_account_type,
               LoginReputationClientResponse::SAFE, 10 * kMinute,
               "test.com/foo/", base::Time::Now());

  EXPECT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));

  CacheVerdict(GURL("http://www.test.com/bar/index2.html"),
               LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
               reused_password_account_type,
               LoginReputationClientResponse::SAFE, 10 * kMinute,
               "test.com/bar/", base::Time::Now());
  EXPECT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));

  // Now cache a PASSWORD_REUSE_EVENT verdict. Verdict count should not
  // increase.
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::GSUITE);
  reused_password_account_type.set_is_account_syncing(true);
  CacheVerdict(GURL("http://www.test.com/foobar/index3.html"),
               LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
               reused_password_account_type,
               LoginReputationClientResponse::SAFE, 10 * kMinute,
               "test.com/foobar/", base::Time::Now());
  EXPECT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));
  EXPECT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
}

TEST_F(ChromePasswordProtectionServiceTest, TestGetCachedVerdicts) {
  ASSERT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
  ASSERT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));
  ReusedPasswordAccountType reused_password_account_type;
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::GSUITE);
  reused_password_account_type.set_is_account_syncing(true);
  // Prepare 4 verdicts of the same origin with different cache expressions,
  // or password type, one is expired, one is not, one is of a different
  // trigger type, and the other is with a different password type.
  base::Time now = base::Time::Now();
  CacheVerdict(GURL("http://test.com/login.html"),
               LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
               reused_password_account_type,
               LoginReputationClientResponse::SAFE, 10 * kMinute, "test.com/",
               now);
  CacheVerdict(
      GURL("http://test.com/def/index.jsp"),
      LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
      reused_password_account_type, LoginReputationClientResponse::PHISHING,
      10 * kMinute, "test.com/def/",
      base::Time::FromSecondsSinceUnixEpoch(now.InSecondsFSinceUnixEpoch() -
                                            kDay));  // Yesterday, expired.
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::UNKNOWN);
  CacheVerdict(GURL("http://test.com/bar/login.html"),
               LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
               reused_password_account_type,
               LoginReputationClientResponse::PHISHING, 10 * kMinute,
               "test.com/bar/", now);
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::NON_GAIA_ENTERPRISE);
  CacheVerdict(GURL("http://test.com/login.html"),
               LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
               reused_password_account_type,
               LoginReputationClientResponse::SAFE, 10 * kMinute, "test.com/",
               now);

  ASSERT_EQ(3U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
  ASSERT_EQ(1U, GetStoredVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));

  // Return VERDICT_TYPE_UNSPECIFIED if look up for a URL with unknown origin.
  LoginReputationClientResponse actual_verdict;
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::GSUITE);
  EXPECT_EQ(LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED,
            service_->GetCachedVerdict(
                GURL("http://www.unknown.com/"),
                LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                reused_password_account_type, &actual_verdict));
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::NON_GAIA_ENTERPRISE);
  EXPECT_EQ(LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED,
            service_->GetCachedVerdict(
                GURL("http://www.unknown.com/"),
                LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                reused_password_account_type, &actual_verdict));

  // Return SAFE if look up for a URL that matches "test.com" cache expression.
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::GSUITE);
  EXPECT_EQ(LoginReputationClientResponse::SAFE,
            service_->GetCachedVerdict(
                GURL("http://test.com/xyz/foo.jsp"),
                LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                reused_password_account_type, &actual_verdict));
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::NON_GAIA_ENTERPRISE);
  EXPECT_EQ(LoginReputationClientResponse::SAFE,
            service_->GetCachedVerdict(
                GURL("http://test.com/xyz/foo.jsp"),
                LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                reused_password_account_type, &actual_verdict));

  // Return VERDICT_TYPE_UNSPECIFIED if look up for a URL whose variants match
  // test.com/def, but the corresponding verdict is expired, so the most
  // matching unexpired verdict will return SAFE
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::GSUITE);
  EXPECT_EQ(LoginReputationClientResponse::SAFE,
            service_->GetCachedVerdict(
                GURL("http://test.com/def/ghi/index.html"),
                LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                reused_password_account_type, &actual_verdict));

  // Return PHISHING. Matches "test.com/bar/" cache expression.
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::UNKNOWN);
  EXPECT_EQ(LoginReputationClientResponse::PHISHING,
            service_->GetCachedVerdict(
                GURL("http://test.com/bar/foo.jsp"),
                LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
                reused_password_account_type, &actual_verdict));

  // Now cache SAFE verdict for the full path.
  CacheVerdict(GURL("http://test.com/bar/foo.jsp"),
               LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
               reused_password_account_type,
               LoginReputationClientResponse::SAFE, 10 * kMinute,
               "test.com/bar/foo.jsp", now);

  // Return SAFE now. Matches the full cache expression.
  EXPECT_EQ(LoginReputationClientResponse::SAFE,
            service_->GetCachedVerdict(
                GURL("http://test.com/bar/foo.jsp"),
                LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
                reused_password_account_type, &actual_verdict));
}

TEST_F(ChromePasswordProtectionServiceTest, TestDoesNotCacheAboutBlank) {
  ASSERT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
  ReusedPasswordAccountType reused_password_account_type;
  reused_password_account_type.set_account_type(
      ReusedPasswordAccountType::UNKNOWN);

  // Should not actually cache, since about:blank is not valid for reputation
  // computing.
  CacheVerdict(
      GURL("about:blank"), LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
      reused_password_account_type, LoginReputationClientResponse::SAFE,
      10 * kMinute, "about:blank", base::Time::Now());

  EXPECT_EQ(0U, GetStoredVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
}