blob: 2083178b7635dcf6432520912f0737b629c4be7e [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/webui/settings/safety_check_handler.h"
#include <optional>
#include <string>
#include <unordered_map>
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "base/types/strong_alias.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/api/passwords_private/passwords_private_delegate.h"
#include "chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.h"
#include "chrome/browser/extensions/test_extension_service.h"
#include "chrome/browser/ui/webui/help/test_version_updater.h"
#include "chrome/browser/ui/webui/version/version_ui.h"
#include "chrome/common/channel_info.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/extensions/api/passwords_private.h"
#include "chrome/test/base/testing_profile.h"
#include "components/affiliations/core/browser/fake_affiliation_service.h"
#include "components/crx_file/id_util.h"
#include "components/password_manager/core/browser/leak_detection/bulk_leak_check.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_store/test_password_store.h"
#include "components/prefs/pref_service.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/safety_check/test_update_check_helper.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_web_ui.h"
#include "extensions/browser/blocklist_extension_prefs.h"
#include "extensions/browser/blocklist_state.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ui/chromeos/devicetype_utils.h"
#endif
#if BUILDFLAG(IS_MAC)
#include "base/mac/mac_util.h"
#endif
// Components for building event strings.
constexpr char kParent[] = "parent";
constexpr char kUpdates[] = "updates";
constexpr char kPasswords[] = "passwords";
constexpr char kSafeBrowsing[] = "safe-browsing";
constexpr char kExtensions[] = "extensions";
namespace {
using Enabled = base::StrongAlias<class EnabledTag, bool>;
using UserCanDisable = base::StrongAlias<class UserCanDisableTag, bool>;
extensions::api::passwords_private::PasswordUiEntry CreateInsecureCredential(
int id,
extensions::api::passwords_private::CompromiseType type) {
extensions::api::passwords_private::PasswordUiEntry entry;
entry.username = "test" + base::NumberToString(id);
extensions::api::passwords_private::CompromisedInfo compromise_info;
compromise_info.compromise_types.push_back(type);
entry.compromised_info = std::move(compromise_info);
return entry;
}
class TestingSafetyCheckHandler : public SafetyCheckHandler {
public:
using SafetyCheckHandler::AllowJavascript;
using SafetyCheckHandler::DisallowJavascript;
using SafetyCheckHandler::set_web_ui;
using SafetyCheckHandler::SetTimestampDelegateForTesting;
using SafetyCheckHandler::SetVersionUpdaterForTesting;
TestingSafetyCheckHandler(
std::unique_ptr<safety_check::UpdateCheckHelper> update_helper,
std::unique_ptr<VersionUpdater> version_updater,
password_manager::BulkLeakCheckService* leak_service,
extensions::PasswordsPrivateDelegate* passwords_delegate,
extensions::ExtensionPrefs* extension_prefs,
extensions::ExtensionServiceInterface* extension_service,
std::unique_ptr<TimestampDelegate> timestamp_delegate)
: SafetyCheckHandler(std::move(update_helper),
std::move(version_updater),
leak_service,
passwords_delegate,
extension_prefs,
extension_service,
std::move(timestamp_delegate)) {}
};
class TestDestructionVersionUpdater : public TestVersionUpdater {
public:
~TestDestructionVersionUpdater() override { destructor_invoked_ = true; }
void CheckForUpdate(StatusCallback callback, PromoteCallback) override {}
static bool GetDestructorInvoked() { return destructor_invoked_; }
private:
static bool destructor_invoked_;
};
class TestTimestampDelegate : public TimestampDelegate {
public:
base::Time GetSystemTime() override {
// 1 second before midnight Dec 31st 2020, so that -(24h-1s) is still on the
// same day. This test time is hard coded to prevent DST flakiness, see
// crbug.com/1066576.
return base::Time::FromSecondsSinceUnixEpoch(1609459199).LocalMidnight() -
base::Seconds(1);
}
};
bool TestDestructionVersionUpdater::destructor_invoked_ = false;
class TestPasswordsDelegate : public extensions::TestPasswordsPrivateDelegate {
public:
TestPasswordsDelegate() {
store_->Init(/*prefs=*/nullptr, /*affiliated_match_helper=*/nullptr);
presenter_.Init();
}
void TearDown() { store_->ShutdownOnUIThread(); }
void SetBulkLeakCheckService(
password_manager::BulkLeakCheckService* leak_service) {
leak_service_ = leak_service;
}
void SetNumLeakedCredentials(int leaked_password_count,
int muted_credentials = 0) {
DCHECK_LE(muted_credentials, leaked_password_count);
leaked_password_count_ = leaked_password_count;
muted_leaked_password_count_ = muted_credentials;
}
void SetNumPhishedCredentials(int phished_password_count) {
phished_password_count_ = phished_password_count;
}
void SetNumWeakCredentials(int weak_password_count) {
weak_password_count_ = weak_password_count;
}
void SetNumReusedCredentials(int reused_password_count) {
reused_password_count_ = reused_password_count;
}
void SetPasswordCheckState(
extensions::api::passwords_private::PasswordCheckState state) {
state_ = state;
}
void SetProgress(int done, int total) {
done_ = done;
total_ = total;
}
void StoreCompromisedPassword() {
// Compromised credentials can be added only after password form to which
// they corresponds exists.
password_manager::PasswordForm form;
form.signon_realm = std::string("test.com");
form.url = GURL("test.com");
// Credentials have to be unique, so the callback is always invoked.
form.username_value = base::ASCIIToUTF16(
"test" + base::NumberToString(test_credential_counter_++));
form.password_value = u"password";
form.username_element = u"username_element";
form.password_issues = {
{password_manager::InsecureType::kLeaked,
password_manager::InsecurityMetadata(
base::Time(), password_manager::IsMuted(false),
password_manager::TriggerBackendNotification(false))}};
store_->AddLogin(form);
}
std::vector<extensions::api::passwords_private::PasswordUiEntry>
GetInsecureCredentials() override {
std::vector<extensions::api::passwords_private::PasswordUiEntry> insecure;
for (int i = 0; i < leaked_password_count_; ++i) {
insecure.push_back(CreateInsecureCredential(
i, extensions::api::passwords_private::CompromiseType::kLeaked));
if (i < muted_leaked_password_count_) {
insecure[i].compromised_info->is_muted = true;
}
}
for (int i = 0; i < phished_password_count_; ++i) {
insecure.push_back(CreateInsecureCredential(
insecure.size(),
extensions::api::passwords_private::CompromiseType::kPhished));
}
for (int i = 0; i < weak_password_count_; ++i) {
insecure.push_back(CreateInsecureCredential(
insecure.size(),
extensions::api::passwords_private::CompromiseType::kWeak));
}
for (int i = 0; i < reused_password_count_; ++i) {
insecure.push_back(CreateInsecureCredential(
insecure.size(),
extensions::api::passwords_private::CompromiseType::kReused));
}
return insecure;
}
extensions::api::passwords_private::PasswordCheckStatus
GetPasswordCheckStatus() override {
extensions::api::passwords_private::PasswordCheckStatus status;
status.state = state_;
if (total_ != 0) {
status.already_processed = done_;
status.remaining_in_queue = total_ - done_;
}
return status;
}
password_manager::InsecureCredentialsManager* GetInsecureCredentialsManager()
override {
return &credentials_manager_;
}
private:
~TestPasswordsDelegate() override = default;
raw_ptr<password_manager::BulkLeakCheckService> leak_service_ = nullptr;
int leaked_password_count_ = 0;
int muted_leaked_password_count_ = 0;
int phished_password_count_ = 0;
int weak_password_count_ = 0;
int reused_password_count_ = 0;
int done_ = 0;
int total_ = 0;
int test_credential_counter_ = 0;
extensions::api::passwords_private::PasswordCheckState state_ =
extensions::api::passwords_private::PasswordCheckState::kIdle;
scoped_refptr<password_manager::TestPasswordStore> store_ =
base::MakeRefCounted<password_manager::TestPasswordStore>();
affiliations::FakeAffiliationService affiliation_service_;
password_manager::SavedPasswordsPresenter presenter_{
&affiliation_service_, store_, /*account_store=*/nullptr};
password_manager::InsecureCredentialsManager credentials_manager_{
&presenter_, store_, /*account_store=*/nullptr};
};
class TestSafetyCheckExtensionService : public TestExtensionService {
public:
void AddExtensionState(const std::string& extension_id,
Enabled enabled,
UserCanDisable user_can_disable) {
state_map_.emplace(extension_id, ExtensionState{enabled.value(),
user_can_disable.value()});
}
bool IsExtensionEnabled(const std::string& extension_id) const override {
auto it = state_map_.find(extension_id);
if (it == state_map_.end()) {
return false;
}
return it->second.enabled;
}
bool UserCanDisableInstalledExtension(
const std::string& extension_id) override {
auto it = state_map_.find(extension_id);
if (it == state_map_.end()) {
return false;
}
return it->second.user_can_disable;
}
private:
struct ExtensionState {
bool enabled;
bool user_can_disable;
};
std::unordered_map<std::string, ExtensionState> state_map_;
};
} // namespace
class SafetyCheckHandlerTest : public testing::Test {
public:
void SetUp() override;
void TearDown() override;
// Returns a |base::Value::Dict| for safety check status update that
// has the specified |component| and |new_state| if it exists; nullptr
// otherwise.
const base::Value::Dict* GetSafetyCheckStatusChangedWithDataIfExists(
const std::string& component,
int new_state);
std::string GenerateExtensionId(char char_to_repeat);
void VerifyDisplayString(const base::Value::Dict* event,
const std::u16string& expected);
void VerifyDisplayString(const base::Value::Dict* event,
const std::string& expected);
// Replaces any instances of browser name (e.g. Google Chrome, Chromium,
// etc) with "browser" to make sure tests work both on Chromium and
// Google Chrome.
void ReplaceBrowserName(std::u16string* s);
protected:
content::BrowserTaskEnvironment browser_task_environment_;
std::unique_ptr<TestingProfile> profile_;
std::unique_ptr<content::WebContents> web_contents_;
raw_ptr<safety_check::TestUpdateCheckHelper, DanglingUntriaged>
update_helper_ = nullptr;
raw_ptr<TestVersionUpdater, DanglingUntriaged> version_updater_ = nullptr;
std::unique_ptr<password_manager::BulkLeakCheckService> test_leak_service_;
scoped_refptr<TestPasswordsDelegate> test_passwords_delegate_;
raw_ptr<extensions::ExtensionPrefs> test_extension_prefs_ = nullptr;
TestSafetyCheckExtensionService test_extension_service_;
content::TestWebUI test_web_ui_;
std::unique_ptr<TestingSafetyCheckHandler> safety_check_;
base::HistogramTester histogram_tester_;
base::test::ScopedFeatureList feature_list_;
};
void SafetyCheckHandlerTest::SetUp() {
TestingProfile::Builder builder;
profile_ = builder.Build();
web_contents_ = content::WebContents::Create(
content::WebContents::CreateParams(profile_.get()));
// The unique pointer to a TestVersionUpdater gets moved to
// SafetyCheckHandler, but a raw pointer is retained here to change its
// state.
auto update_helper = std::make_unique<safety_check::TestUpdateCheckHelper>();
update_helper_ = update_helper.get();
auto version_updater = std::make_unique<TestVersionUpdater>();
version_updater_ = version_updater.get();
test_leak_service_ = std::make_unique<password_manager::BulkLeakCheckService>(
nullptr, nullptr);
test_passwords_delegate_ = base::MakeRefCounted<TestPasswordsDelegate>();
test_passwords_delegate_->SetBulkLeakCheckService(test_leak_service_.get());
test_web_ui_.set_web_contents(web_contents_.get());
test_extension_prefs_ = extensions::ExtensionPrefs::Get(profile_.get());
auto timestamp_delegate = std::make_unique<TestTimestampDelegate>();
safety_check_ = std::make_unique<TestingSafetyCheckHandler>(
std::move(update_helper), std::move(version_updater),
test_leak_service_.get(), test_passwords_delegate_.get(),
test_extension_prefs_, &test_extension_service_,
std::move(timestamp_delegate));
test_web_ui_.ClearTrackedCalls();
safety_check_->set_web_ui(&test_web_ui_);
safety_check_->AllowJavascript();
browser_task_environment_.RunUntilIdle();
}
void SafetyCheckHandlerTest::TearDown() {
test_passwords_delegate_->TearDown();
browser_task_environment_.RunUntilIdle();
}
const base::Value::Dict*
SafetyCheckHandlerTest::GetSafetyCheckStatusChangedWithDataIfExists(
const std::string& component,
int new_state) {
// Return the latest update if multiple, so iterate from the end.
const std::vector<std::unique_ptr<content::TestWebUI::CallData>>& call_data =
test_web_ui_.call_data();
for (int i = call_data.size() - 1; i >= 0; --i) {
const content::TestWebUI::CallData& data = *(call_data[i]);
if (data.function_name() != "cr.webUIListenerCallback") {
continue;
}
const std::string* event = data.arg1()->GetIfString();
if (!event || *event != "safety-check-" + component + "-status-changed")
continue;
const base::Value::Dict* dictionary = data.arg2()->GetIfDict();
if (!dictionary) {
continue;
}
std::optional<int> cur_new_state = dictionary->FindInt("newState");
if (cur_new_state == new_state)
return dictionary;
}
return nullptr;
}
std::string SafetyCheckHandlerTest::GenerateExtensionId(char char_to_repeat) {
return std::string(crx_file::id_util::kIdSize * 2, char_to_repeat);
}
void SafetyCheckHandlerTest::VerifyDisplayString(
const base::Value::Dict* event,
const std::u16string& expected) {
const std::string* display_ptr = event->FindString("displayString");
ASSERT_TRUE(display_ptr);
std::u16string display = base::UTF8ToUTF16(*display_ptr);
ReplaceBrowserName(&display);
// Need to also replace any instances of Chrome and Chromium in the
// expected string due to an edge case on ChromeOS, where a device name
// is "Chrome", which gets replaced in the display string.
std::u16string expected_replaced = expected;
ReplaceBrowserName(&expected_replaced);
EXPECT_EQ(expected_replaced, display);
}
void SafetyCheckHandlerTest::VerifyDisplayString(const base::Value::Dict* event,
const std::string& expected) {
VerifyDisplayString(event, base::ASCIIToUTF16(expected));
}
void SafetyCheckHandlerTest::ReplaceBrowserName(std::u16string* s) {
base::ReplaceSubstringsAfterOffset(s, 0, u"Google Chrome", u"Browser");
base::ReplaceSubstringsAfterOffset(s, 0, u"Chrome", u"Browser");
base::ReplaceSubstringsAfterOffset(s, 0, u"Chromium", u"Browser");
}
TEST_F(SafetyCheckHandlerTest, CheckUpdates_Checking) {
version_updater_->SetReturnedStatus(VersionUpdater::Status::CHECKING);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kUpdates, static_cast<int>(SafetyCheckHandler::UpdateStatus::kChecking));
ASSERT_TRUE(event);
VerifyDisplayString(event, u"");
// Checking state should not get recorded.
histogram_tester_.ExpectTotalCount("Settings.SafetyCheck.UpdatesResult", 0);
}
TEST_F(SafetyCheckHandlerTest, CheckUpdates_Updated) {
version_updater_->SetReturnedStatus(VersionUpdater::Status::UPDATED);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kUpdates, static_cast<int>(SafetyCheckHandler::UpdateStatus::kUpdated));
ASSERT_TRUE(event);
#if BUILDFLAG(IS_CHROMEOS_ASH)
std::u16string expected =
u"Your " + ui::GetChromeOSDeviceName() + u" is up to date";
VerifyDisplayString(event, expected);
#else
VerifyDisplayString(event, "Browser is up to date");
#endif
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.UpdatesResult",
SafetyCheckHandler::UpdateStatus::kUpdated, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckUpdates_Updating) {
version_updater_->SetReturnedStatus(VersionUpdater::Status::UPDATING);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kUpdates, static_cast<int>(SafetyCheckHandler::UpdateStatus::kUpdating));
ASSERT_TRUE(event);
#if BUILDFLAG(IS_CHROMEOS_ASH)
VerifyDisplayString(event, "Updating your device");
#else
VerifyDisplayString(event, "Updating Browser");
#endif
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.UpdatesResult",
SafetyCheckHandler::UpdateStatus::kUpdating, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckUpdates_Relaunch) {
version_updater_->SetReturnedStatus(VersionUpdater::Status::NEARLY_UPDATED);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kUpdates, static_cast<int>(SafetyCheckHandler::UpdateStatus::kRelaunch));
ASSERT_TRUE(event);
#if BUILDFLAG(IS_CHROMEOS_ASH)
VerifyDisplayString(
event, "Nearly up to date! Restart your device to finish updating.");
#else
VerifyDisplayString(event,
"Nearly up to date! Relaunch Browser to finish "
"updating.");
#endif
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.UpdatesResult",
SafetyCheckHandler::UpdateStatus::kRelaunch, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckUpdates_Disabled) {
version_updater_->SetReturnedStatus(VersionUpdater::Status::DISABLED);
safety_check_->PerformSafetyCheck();
// TODO(crbug/1072432): Since the UNKNOWN state is not present in JS in M83,
// use FAILED_OFFLINE, which uses the same icon.
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kUpdates,
static_cast<int>(SafetyCheckHandler::UpdateStatus::kFailedOffline));
ASSERT_TRUE(event);
VerifyDisplayString(
event, base::UTF16ToUTF8(VersionUI::GetAnnotatedVersionStringForUi()));
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.UpdatesResult",
SafetyCheckHandler::UpdateStatus::kUnknown, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckUpdates_DisabledByAdmin) {
version_updater_->SetReturnedStatus(
VersionUpdater::Status::DISABLED_BY_ADMIN);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kUpdates,
static_cast<int>(SafetyCheckHandler::UpdateStatus::kDisabledByAdmin));
ASSERT_TRUE(event);
VerifyDisplayString(
event,
"Updates are managed by <a target=\"_blank\" "
"href=\"https://support.google.com/chrome?p=your_administrator\">your "
"administrator</a>");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.UpdatesResult",
SafetyCheckHandler::UpdateStatus::kDisabledByAdmin, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckUpdates_FailedOffline) {
version_updater_->SetReturnedStatus(VersionUpdater::Status::FAILED_OFFLINE);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kUpdates,
static_cast<int>(SafetyCheckHandler::UpdateStatus::kFailedOffline));
ASSERT_TRUE(event);
VerifyDisplayString(event,
"Browser can't check for updates. Try checking your "
"internet connection.");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.UpdatesResult",
SafetyCheckHandler::UpdateStatus::kFailedOffline, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckUpdates_Failed_ConnectivityOnline) {
update_helper_->SetConnectivity(true);
version_updater_->SetReturnedStatus(VersionUpdater::Status::FAILED);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kUpdates, static_cast<int>(SafetyCheckHandler::UpdateStatus::kFailed));
ASSERT_TRUE(event);
VerifyDisplayString(
event,
"Browser didn't update, something went wrong. <a target=\"_blank\" "
"href=\"https://support.google.com/chrome?p=fix_chrome_updates\">Fix "
"Browser update problems and failed updates.</a>");
histogram_tester_.ExpectBucketCount("Settings.SafetyCheck.UpdatesResult",
SafetyCheckHandler::UpdateStatus::kFailed,
1);
}
TEST_F(SafetyCheckHandlerTest, CheckUpdates_Failed_ConnectivityOffline) {
update_helper_->SetConnectivity(false);
version_updater_->SetReturnedStatus(VersionUpdater::Status::FAILED);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kUpdates,
static_cast<int>(SafetyCheckHandler::UpdateStatus::kFailedOffline));
ASSERT_TRUE(event);
VerifyDisplayString(event,
"Browser can't check for updates. Try checking your "
"internet connection.");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.UpdatesResult",
SafetyCheckHandler::UpdateStatus::kFailedOffline, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckUpdates_DestroyedOnJavascriptDisallowed) {
EXPECT_FALSE(TestDestructionVersionUpdater::GetDestructorInvoked());
safety_check_->SetVersionUpdaterForTesting(
std::make_unique<TestDestructionVersionUpdater>());
safety_check_->PerformSafetyCheck();
safety_check_->DisallowJavascript();
EXPECT_TRUE(TestDestructionVersionUpdater::GetDestructorInvoked());
}
TEST_F(SafetyCheckHandlerTest, CheckUpdates_UpdateToRollbackVersionDisallowed) {
version_updater_->SetReturnedStatus(
VersionUpdater::Status::UPDATE_TO_ROLLBACK_VERSION_DISALLOWED);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kUpdates, static_cast<int>(SafetyCheckHandler::UpdateStatus::
kUpdateToRollbackVersionDisallowed));
ASSERT_TRUE(event);
VerifyDisplayString(
event,
"You reverted to a previous version of ChromeOS. "
"To get updates, wait until the next version is available.");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.UpdatesResult",
SafetyCheckHandler::UpdateStatus::kUpdateToRollbackVersionDisallowed, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckSafeBrowsing_EnabledStandard) {
TestingProfile::FromWebUI(&test_web_ui_)
->AsTestingProfile()
->GetTestingPrefService()
->SetManagedPref(prefs::kSafeBrowsingEnabled,
std::make_unique<base::Value>(true));
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kSafeBrowsing,
static_cast<int>(
SafetyCheckHandler::SafeBrowsingStatus::kEnabledStandard));
ASSERT_TRUE(event);
VerifyDisplayString(event, "Standard Protection is on");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.SafeBrowsingResult",
SafetyCheckHandler::SafeBrowsingStatus::kEnabledStandard, 1);
}
TEST_F(SafetyCheckHandlerTest,
CheckSafeBrowsing_EnabledStandardAvailableEnhanced) {
Profile::FromWebUI(&test_web_ui_)
->GetPrefs()
->SetBoolean(prefs::kSafeBrowsingEnabled, true);
Profile::FromWebUI(&test_web_ui_)
->GetPrefs()
->SetBoolean(prefs::kSafeBrowsingEnhanced, false);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kSafeBrowsing, static_cast<int>(SafetyCheckHandler::SafeBrowsingStatus::
kEnabledStandardAvailableEnhanced));
ASSERT_TRUE(event);
VerifyDisplayString(event,
"Standard protection is on. For even more security, use "
"enhanced protection.");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.SafeBrowsingResult",
SafetyCheckHandler::SafeBrowsingStatus::kEnabledStandardAvailableEnhanced,
1);
}
TEST_F(SafetyCheckHandlerTest, CheckSafeBrowsing_EnabledEnhanced) {
Profile::FromWebUI(&test_web_ui_)
->GetPrefs()
->SetBoolean(prefs::kSafeBrowsingEnabled, true);
Profile::FromWebUI(&test_web_ui_)
->GetPrefs()
->SetBoolean(prefs::kSafeBrowsingEnhanced, true);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kSafeBrowsing,
static_cast<int>(
SafetyCheckHandler::SafeBrowsingStatus::kEnabledEnhanced));
ASSERT_TRUE(event);
VerifyDisplayString(event, "Enhanced Protection is on");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.SafeBrowsingResult",
SafetyCheckHandler::SafeBrowsingStatus::kEnabledEnhanced, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckSafeBrowsing_InconsistentEnhanced) {
// Tests the corner case of SafeBrowsingEnhanced pref being on, while
// SafeBrowsing enabled is off. This should be treated as SB off.
Profile::FromWebUI(&test_web_ui_)
->GetPrefs()
->SetBoolean(prefs::kSafeBrowsingEnabled, false);
Profile::FromWebUI(&test_web_ui_)
->GetPrefs()
->SetBoolean(prefs::kSafeBrowsingEnhanced, true);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kSafeBrowsing,
static_cast<int>(SafetyCheckHandler::SafeBrowsingStatus::kDisabled));
ASSERT_TRUE(event);
VerifyDisplayString(
event, "Safe Browsing is off. Browser recommends turning it on.");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.SafeBrowsingResult",
SafetyCheckHandler::SafeBrowsingStatus::kDisabled, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckSafeBrowsing_Disabled) {
Profile::FromWebUI(&test_web_ui_)
->GetPrefs()
->SetBoolean(prefs::kSafeBrowsingEnabled, false);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kSafeBrowsing,
static_cast<int>(SafetyCheckHandler::SafeBrowsingStatus::kDisabled));
ASSERT_TRUE(event);
VerifyDisplayString(
event, "Safe Browsing is off. Browser recommends turning it on.");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.SafeBrowsingResult",
SafetyCheckHandler::SafeBrowsingStatus::kDisabled, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckSafeBrowsing_DisabledByAdmin) {
TestingProfile::FromWebUI(&test_web_ui_)
->AsTestingProfile()
->GetTestingPrefService()
->SetManagedPref(prefs::kSafeBrowsingEnabled,
std::make_unique<base::Value>(false));
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kSafeBrowsing,
static_cast<int>(
SafetyCheckHandler::SafeBrowsingStatus::kDisabledByAdmin));
ASSERT_TRUE(event);
VerifyDisplayString(
event,
"<a target=\"_blank\" "
"href=\"https://support.google.com/chrome?p=your_administrator\">Your "
"administrator</a> has turned off Safe Browsing");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.SafeBrowsingResult",
SafetyCheckHandler::SafeBrowsingStatus::kDisabledByAdmin, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckSafeBrowsing_DisabledByExtension) {
TestingProfile::FromWebUI(&test_web_ui_)
->AsTestingProfile()
->GetTestingPrefService()
->SetExtensionPref(prefs::kSafeBrowsingEnabled,
std::make_unique<base::Value>(false));
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kSafeBrowsing,
static_cast<int>(
SafetyCheckHandler::SafeBrowsingStatus::kDisabledByExtension));
ASSERT_TRUE(event);
VerifyDisplayString(event, "An extension has turned off Safe Browsing");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.SafeBrowsingResult",
SafetyCheckHandler::SafeBrowsingStatus::kDisabledByExtension, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_ObserverRemovedAfterError) {
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking));
ASSERT_TRUE(event);
VerifyDisplayString(event, u"");
histogram_tester_.ExpectTotalCount("Settings.SafetyCheck.PasswordsResult2",
0);
// Second, an "offline" state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kNetworkError);
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kOffline));
ASSERT_TRUE(event2);
VerifyDisplayString(event2,
"Browser can't check your passwords. Try checking your "
"internet connection.");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kOffline, 1);
// Another error, but since the previous state is terminal, the handler
// should no longer be observing the BulkLeakCheckService state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kServiceError);
const base::Value::Dict* event3 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kOffline));
ASSERT_TRUE(event3);
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kOffline, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_InterruptedAndRefreshed) {
safety_check_->PerformSafetyCheck();
// Password check running.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking));
ASSERT_TRUE(event);
VerifyDisplayString(event, u"");
// The check gets interrupted and the page is refreshed.
safety_check_->DisallowJavascript();
safety_check_->AllowJavascript();
// Need to set the |TestVersionUpdater| instance again to prevent
// |PerformSafetyCheck()| from creating a real |VersionUpdater| instance.
safety_check_->SetVersionUpdaterForTesting(
std::make_unique<TestVersionUpdater>());
// Another run of the safety check.
safety_check_->PerformSafetyCheck();
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking));
ASSERT_TRUE(event2);
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kSignedOut);
const base::Value::Dict* event3 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kSignedOut));
ASSERT_TRUE(event3);
VerifyDisplayString(event3,
"Browser can't check your passwords because you're not "
"signed in");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kSignedOut, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_StartedTwice) {
safety_check_->PerformSafetyCheck();
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking));
ASSERT_TRUE(event);
// Then, a network error.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kNetworkError);
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kOffline));
EXPECT_TRUE(event2);
VerifyDisplayString(event2,
"Browser can't check your passwords. Try checking your "
"internet connection.");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kOffline, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_ObserverNotifiedTwice) {
safety_check_->PerformSafetyCheck();
EXPECT_TRUE(test_passwords_delegate_->StartPasswordCheckTriggered());
static_cast<password_manager::BulkLeakCheckService::Observer*>(
safety_check_.get())
->OnStateChanged(
password_manager::BulkLeakCheckService::State::kServiceError);
// Another notification about the same state change.
static_cast<password_manager::BulkLeakCheckService::Observer*>(
safety_check_.get())
->OnStateChanged(
password_manager::BulkLeakCheckService::State::kServiceError);
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kError));
ASSERT_TRUE(event);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_Safe) {
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking)));
// Second, a "safe" state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kIdle);
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords, static_cast<int>(SafetyCheckHandler::PasswordsStatus::kSafe));
EXPECT_TRUE(event);
VerifyDisplayString(event, "No compromised passwords found");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kSafe, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_StaleSafeThenCompromised) {
constexpr int kCompromised = 7;
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
test_passwords_delegate_->SetPasswordCheckState(
extensions::api::passwords_private::PasswordCheckState::kRunning);
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking)));
// Not a "safe" state, so send an |OnCredentialDone| with is_leaked=true.
static_cast<password_manager::BulkLeakCheckService::Observer*>(
safety_check_.get())
->OnCredentialDone({u"login", u"password"},
password_manager::IsLeaked(true));
// The service goes idle, but the disk still has a stale "safe" state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kIdle);
test_passwords_delegate_->SetPasswordCheckState(
extensions::api::passwords_private::PasswordCheckState::kIdle);
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords, static_cast<int>(SafetyCheckHandler::PasswordsStatus::kSafe));
EXPECT_TRUE(event);
// An InsecureCredentialsManager callback fires once the compromised passwords
// get written to disk.
test_passwords_delegate_->SetNumLeakedCredentials(kCompromised);
test_passwords_delegate_->StoreCompromisedPassword();
browser_task_environment_.RunUntilIdle();
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kCompromisedExist));
ASSERT_TRUE(event2);
VerifyDisplayString(
event2, base::NumberToString(kCompromised) + " compromised passwords");
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_SafeStateThenMoreEvents) {
safety_check_->PerformSafetyCheck();
// Running state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
test_passwords_delegate_->SetPasswordCheckState(
extensions::api::passwords_private::PasswordCheckState::kRunning);
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking)));
// Previous safe state got loaded.
test_passwords_delegate_->SetNumLeakedCredentials(0);
test_passwords_delegate_->StoreCompromisedPassword();
browser_task_environment_.RunUntilIdle();
// The event should get ignored, since the state is still running.
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords, static_cast<int>(SafetyCheckHandler::PasswordsStatus::kSafe));
EXPECT_FALSE(event);
// The check is completed with another safe state.
test_passwords_delegate_->SetPasswordCheckState(
extensions::api::passwords_private::PasswordCheckState::kIdle);
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kIdle);
// This time the safe state should be reflected.
event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords, static_cast<int>(SafetyCheckHandler::PasswordsStatus::kSafe));
EXPECT_TRUE(event);
// After some time, some compromises were discovered (unrelated to SC).
constexpr int kCompromised = 7;
test_passwords_delegate_->SetNumLeakedCredentials(kCompromised);
test_passwords_delegate_->StoreCompromisedPassword();
browser_task_environment_.RunUntilIdle();
// The new event should get ignored, since the safe state was final.
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kCompromisedExist));
EXPECT_FALSE(event2);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_OnlyLeakedExist) {
constexpr int kLeaked = 7;
test_passwords_delegate_->SetNumLeakedCredentials(kLeaked);
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking)));
// Compromised passwords found state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kIdle);
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kCompromisedExist));
ASSERT_TRUE(event2);
VerifyDisplayString(event2,
base::NumberToString(kLeaked) + " compromised passwords");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kCompromisedExist, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_OnlyPhishedExist) {
constexpr int kPhished = 7;
test_passwords_delegate_->SetNumPhishedCredentials(kPhished);
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking)));
// Compromised passwords found state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kIdle);
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kCompromisedExist));
ASSERT_TRUE(event2);
VerifyDisplayString(
event2, base::NumberToString(kPhished) + " compromised passwords");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kCompromisedExist, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_LeakedAndPhishedExist) {
constexpr int kLeaked = 7, kPhished = 7;
test_passwords_delegate_->SetNumLeakedCredentials(kLeaked);
test_passwords_delegate_->SetNumPhishedCredentials(kPhished);
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking)));
// Compromised passwords found state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kIdle);
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kCompromisedExist));
ASSERT_TRUE(event2);
VerifyDisplayString(event2, base::NumberToString(kLeaked + kPhished) +
" compromised passwords");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kCompromisedExist, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_CompromisedAndWeakExist) {
constexpr int kCompromised = 7;
constexpr int kWeak = 13;
test_passwords_delegate_->SetNumLeakedCredentials(kCompromised);
test_passwords_delegate_->SetNumWeakCredentials(kWeak);
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking)));
// Compromised passwords found state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kIdle);
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kCompromisedExist));
ASSERT_TRUE(event2);
VerifyDisplayString(
event2, base::NumberToString(kCompromised) + " compromised passwords, " +
base::NumberToString(kWeak) + " weak passwords");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kCompromisedExist, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_CompromisedAndReusedExist) {
constexpr int kCompromised = 7;
constexpr int kReused = 13;
test_passwords_delegate_->SetNumLeakedCredentials(kCompromised);
test_passwords_delegate_->SetNumReusedCredentials(kReused);
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking)));
// Compromised passwords found state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kIdle);
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kCompromisedExist));
ASSERT_TRUE(event2);
VerifyDisplayString(
event2, base::NumberToString(kCompromised) + " compromised passwords, " +
base::NumberToString(kReused) + " reused passwords");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kCompromisedExist, 1);
}
TEST_F(SafetyCheckHandlerTest,
CheckPasswords_CompromisedAndWeakAndReusedExist) {
constexpr int kCompromised = 7;
constexpr int kWeak = 13;
constexpr int kReused = 6;
test_passwords_delegate_->SetNumLeakedCredentials(kCompromised);
test_passwords_delegate_->SetNumWeakCredentials(kWeak);
test_passwords_delegate_->SetNumReusedCredentials(kReused);
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking)));
// Compromised passwords found state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kIdle);
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kCompromisedExist));
ASSERT_TRUE(event2);
VerifyDisplayString(
event2, base::NumberToString(kCompromised) + " compromised passwords, " +
base::NumberToString(kWeak) + " weak passwords, " +
base::NumberToString(kReused) + " reused passwords");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kCompromisedExist, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_WeakAndReusedExist) {
constexpr int kWeak = 13;
constexpr int kReused = 6;
test_passwords_delegate_->SetNumWeakCredentials(kWeak);
test_passwords_delegate_->SetNumReusedCredentials(kReused);
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking)));
// Compromised passwords found state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kIdle);
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(
SafetyCheckHandler::PasswordsStatus::kWeakPasswordsExist));
ASSERT_TRUE(event2);
VerifyDisplayString(event2,
base::NumberToString(kWeak) + " weak passwords, " +
base::NumberToString(kReused) + " reused passwords");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kWeakPasswordsExist, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_OnlyWeakExist) {
constexpr int kWeak = 13;
test_passwords_delegate_->SetNumWeakCredentials(kWeak);
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking)));
// Compromised passwords found state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kIdle);
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(
SafetyCheckHandler::PasswordsStatus::kWeakPasswordsExist));
ASSERT_TRUE(event2);
VerifyDisplayString(event2, base::NumberToString(kWeak) + " weak passwords");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kWeakPasswordsExist, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_OnlyReusedExist) {
constexpr int kReused = 13;
test_passwords_delegate_->SetNumReusedCredentials(kReused);
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking)));
// Compromised passwords found state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kIdle);
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(
SafetyCheckHandler::PasswordsStatus::kReusedPasswordsExist));
ASSERT_TRUE(event);
VerifyDisplayString(event,
base::NumberToString(kReused) + " reused passwords");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kReusedPasswordsExist, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_Error) {
safety_check_->PerformSafetyCheck();
EXPECT_TRUE(test_passwords_delegate_->StartPasswordCheckTriggered());
static_cast<password_manager::BulkLeakCheckService::Observer*>(
safety_check_.get())
->OnStateChanged(
password_manager::BulkLeakCheckService::State::kServiceError);
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kError));
ASSERT_TRUE(event);
VerifyDisplayString(event,
"Browser can't check your passwords. Try again "
"later.");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kError, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_MutedCompromisedExist) {
constexpr int kCompromised = 7;
constexpr int kMuted = 3;
test_passwords_delegate_->SetNumLeakedCredentials(kCompromised, kMuted);
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking)));
// Compromised passwords found state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kIdle);
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kCompromisedExist));
ASSERT_TRUE(event2);
VerifyDisplayString(event2, base::NumberToString(kCompromised - kMuted) +
" compromised passwords");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kCompromisedExist, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_AllMutedCompromisedCredentials) {
constexpr int kCompromised = 7;
test_passwords_delegate_->SetNumLeakedCredentials(kCompromised, kCompromised);
safety_check_->PerformSafetyCheck();
// First, a "running" change of state.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kRunning);
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking)));
// Compromised passwords not found.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kIdle);
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords, static_cast<int>(SafetyCheckHandler::PasswordsStatus::kSafe));
ASSERT_TRUE(event);
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kSafe, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_Error_FutureEventsIgnored) {
safety_check_->PerformSafetyCheck();
EXPECT_TRUE(test_passwords_delegate_->StartPasswordCheckTriggered());
static_cast<password_manager::BulkLeakCheckService::Observer*>(
safety_check_.get())
->OnStateChanged(
password_manager::BulkLeakCheckService::State::kServiceError);
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kError));
ASSERT_TRUE(event);
VerifyDisplayString(event,
"Browser can't check your passwords. Try again "
"later.");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kError, 1);
// At some point later, the service discovers compromised passwords and goes
// idle.
constexpr int kCompromised = 7;
static_cast<password_manager::BulkLeakCheckService::Observer*>(
safety_check_.get())
->OnStateChanged(password_manager::BulkLeakCheckService::State::kRunning);
static_cast<password_manager::BulkLeakCheckService::Observer*>(
safety_check_.get())
->OnStateChanged(password_manager::BulkLeakCheckService::State::kIdle);
// An InsecureCredentialsManager callback fires once the compromised passwords
// get written to disk.
test_passwords_delegate_->SetNumLeakedCredentials(kCompromised);
test_passwords_delegate_->StoreCompromisedPassword();
browser_task_environment_.RunUntilIdle();
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kCompromisedExist));
// The event for compromised passwords should not exist, since the changes
// should no longer be observed.
EXPECT_FALSE(event2);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_FeatureUnavailable) {
safety_check_->PerformSafetyCheck();
EXPECT_TRUE(test_passwords_delegate_->StartPasswordCheckTriggered());
static_cast<password_manager::BulkLeakCheckService::Observer*>(
safety_check_.get())
->OnStateChanged(
password_manager::BulkLeakCheckService::State::kTokenRequestFailure);
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(
SafetyCheckHandler::PasswordsStatus::kFeatureUnavailable));
ASSERT_TRUE(event);
VerifyDisplayString(event, "Password check is not available in Chromium");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kFeatureUnavailable, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_RunningOneCompromised) {
test_passwords_delegate_->SetNumLeakedCredentials(1);
safety_check_->PerformSafetyCheck();
EXPECT_TRUE(test_passwords_delegate_->StartPasswordCheckTriggered());
static_cast<password_manager::BulkLeakCheckService::Observer*>(
safety_check_.get())
->OnStateChanged(password_manager::BulkLeakCheckService::State::kIdle);
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kCompromisedExist));
ASSERT_TRUE(event);
VerifyDisplayString(event, "1 compromised password");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kCompromisedExist, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_NoPasswords) {
test_passwords_delegate_->ClearSavedPasswordsList();
test_passwords_delegate_->SetStartPasswordCheckState(
password_manager::BulkLeakCheckService::State::kIdle);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kNoPasswords));
EXPECT_TRUE(event);
VerifyDisplayString(event,
"No saved passwords. Chrome can check your passwords "
"when you save them.");
histogram_tester_.ExpectBucketCount(
"Settings.SafetyCheck.PasswordsResult2",
SafetyCheckHandler::PasswordsStatus::kNoPasswords, 1);
}
TEST_F(SafetyCheckHandlerTest, CheckPasswords_Progress) {
auto credential = password_manager::LeakCheckCredential(u"test", u"test");
auto is_leaked = password_manager::IsLeaked(false);
safety_check_->PerformSafetyCheck();
test_passwords_delegate_->SetPasswordCheckState(
extensions::api::passwords_private::PasswordCheckState::kRunning);
test_passwords_delegate_->SetProgress(1, 3);
static_cast<password_manager::BulkLeakCheckService::Observer*>(
safety_check_.get())
->OnCredentialDone(credential, is_leaked);
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking));
EXPECT_TRUE(event);
VerifyDisplayString(event, u"Checking passwords (1 of 3)…");
test_passwords_delegate_->SetProgress(2, 3);
static_cast<password_manager::BulkLeakCheckService::Observer*>(
safety_check_.get())
->OnCredentialDone(credential, is_leaked);
const base::Value::Dict* event2 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking));
EXPECT_TRUE(event2);
VerifyDisplayString(event2, u"Checking passwords (2 of 3)…");
// Final update comes after status change, so no new progress message should
// be present.
test_passwords_delegate_->SetPasswordCheckState(
extensions::api::passwords_private::PasswordCheckState::kIdle);
test_passwords_delegate_->SetProgress(3, 3);
static_cast<password_manager::BulkLeakCheckService::Observer*>(
safety_check_.get())
->OnCredentialDone(credential, is_leaked);
const base::Value::Dict* event3 = GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking));
EXPECT_TRUE(event3);
// Still 2/3 event.
VerifyDisplayString(event3, u"Checking passwords (2 of 3)…");
}
TEST_F(SafetyCheckHandlerTest, CheckExtensions_NoExtensions) {
safety_check_->PerformSafetyCheck();
EXPECT_TRUE(GetSafetyCheckStatusChangedWithDataIfExists(
kExtensions,
static_cast<int>(
SafetyCheckHandler::ExtensionsStatus::kNoneBlocklisted)));
}
TEST_F(SafetyCheckHandlerTest, CheckExtensions_NoneBlocklisted) {
std::string extension_id = GenerateExtensionId('a');
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder(extension_id).Build();
test_extension_prefs_->OnExtensionInstalled(
extension.get(), extensions::Extension::State::ENABLED,
syncer::StringOrdinal(), "");
extensions::blocklist_prefs::SetSafeBrowsingExtensionBlocklistState(
extension_id, extensions::BitMapBlocklistState::NOT_BLOCKLISTED,
test_extension_prefs_);
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kExtensions,
static_cast<int>(SafetyCheckHandler::ExtensionsStatus::kNoneBlocklisted));
EXPECT_TRUE(event);
VerifyDisplayString(event,
"You're protected from potentially harmful extensions");
}
TEST_F(SafetyCheckHandlerTest, CheckExtensions_BlocklistedAllDisabled) {
std::string extension_id = GenerateExtensionId('a');
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("test0").SetID(extension_id).Build();
test_extension_prefs_->OnExtensionInstalled(
extension.get(), extensions::Extension::State::DISABLED,
syncer::StringOrdinal(), "");
extensions::blocklist_prefs::SetSafeBrowsingExtensionBlocklistState(
extension_id, extensions::BitMapBlocklistState::BLOCKLISTED_MALWARE,
test_extension_prefs_);
test_extension_service_.AddExtensionState(extension_id, Enabled(false),
UserCanDisable(false));
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kExtensions,
static_cast<int>(
SafetyCheckHandler::ExtensionsStatus::kBlocklistedAllDisabled));
EXPECT_TRUE(event);
VerifyDisplayString(
event, "1 potentially harmful extension is off. You can also remove it.");
}
TEST_F(SafetyCheckHandlerTest, CheckExtensions_BlocklistedReenabledAllByUser) {
std::string extension_id = GenerateExtensionId('a');
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("test0").SetID(extension_id).Build();
test_extension_prefs_->OnExtensionInstalled(
extension.get(), extensions::Extension::State::ENABLED,
syncer::StringOrdinal(), "");
extensions::blocklist_prefs::SetSafeBrowsingExtensionBlocklistState(
extension_id,
extensions::BitMapBlocklistState::BLOCKLISTED_POTENTIALLY_UNWANTED,
test_extension_prefs_);
test_extension_service_.AddExtensionState(extension_id, Enabled(true),
UserCanDisable(true));
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kExtensions, static_cast<int>(SafetyCheckHandler::ExtensionsStatus::
kBlocklistedReenabledAllByUser));
EXPECT_TRUE(event);
VerifyDisplayString(event,
"You turned 1 potentially harmful extension back on");
}
TEST_F(SafetyCheckHandlerTest, CheckExtensions_BlocklistedReenabledAllByAdmin) {
std::string extension_id = GenerateExtensionId('a');
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("test0").SetID(extension_id).Build();
test_extension_prefs_->OnExtensionInstalled(
extension.get(), extensions::Extension::State::ENABLED,
syncer::StringOrdinal(), "");
extensions::blocklist_prefs::SetSafeBrowsingExtensionBlocklistState(
extension_id,
extensions::BitMapBlocklistState::BLOCKLISTED_POTENTIALLY_UNWANTED,
test_extension_prefs_);
test_extension_service_.AddExtensionState(extension_id, Enabled(true),
UserCanDisable(false));
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kExtensions, static_cast<int>(SafetyCheckHandler::ExtensionsStatus::
kBlocklistedReenabledAllByAdmin));
VerifyDisplayString(event,
"Your administrator turned 1 potentially harmful "
"extension back on");
}
TEST_F(SafetyCheckHandlerTest, CheckExtensions_BlocklistedReenabledSomeByUser) {
std::string extension_id = GenerateExtensionId('a');
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("test0").SetID(extension_id).Build();
test_extension_prefs_->OnExtensionInstalled(
extension.get(), extensions::Extension::State::ENABLED,
syncer::StringOrdinal(), "");
extensions::blocklist_prefs::SetSafeBrowsingExtensionBlocklistState(
extension_id,
extensions::BitMapBlocklistState::BLOCKLISTED_POTENTIALLY_UNWANTED,
test_extension_prefs_);
test_extension_service_.AddExtensionState(extension_id, Enabled(true),
UserCanDisable(true));
std::string extension2_id = GenerateExtensionId('b');
scoped_refptr<const extensions::Extension> extension2 =
extensions::ExtensionBuilder("test1").SetID(extension2_id).Build();
test_extension_prefs_->OnExtensionInstalled(
extension2.get(), extensions::Extension::State::ENABLED,
syncer::StringOrdinal(), "");
extensions::blocklist_prefs::SetSafeBrowsingExtensionBlocklistState(
extension2_id,
extensions::BitMapBlocklistState::BLOCKLISTED_POTENTIALLY_UNWANTED,
test_extension_prefs_);
test_extension_service_.AddExtensionState(extension2_id, Enabled(true),
UserCanDisable(false));
safety_check_->PerformSafetyCheck();
const base::Value::Dict* event = GetSafetyCheckStatusChangedWithDataIfExists(
kExtensions, static_cast<int>(SafetyCheckHandler::ExtensionsStatus::
kBlocklistedReenabledSomeByUser));
EXPECT_TRUE(event);
VerifyDisplayString(event,
"You turned 1 potentially harmful extension back "
"on. Your administrator "
"turned 1 potentially harmful extension back on.");
}
TEST_F(SafetyCheckHandlerTest, CheckParentRanDisplayString) {
// 1 second before midnight Dec 31st 2020, so that -(24h-1s) is still on the
// same day. This test time is hard coded to prevent DST flakiness, see
// crbug.com/1066576.
const base::Time system_time =
base::Time::FromSecondsSinceUnixEpoch(1609459199).LocalMidnight() -
base::Seconds(1);
// Display strings for given time deltas in seconds.
std::vector<std::tuple<std::u16string, int>> tuples{
std::make_tuple(u"a moment ago", 1),
std::make_tuple(u"a moment ago", 59),
std::make_tuple(u"1 minute ago", 60),
std::make_tuple(u"2 minutes ago", 60 * 2),
std::make_tuple(u"59 minutes ago", 60 * 60 - 1),
std::make_tuple(u"1 hour ago", 60 * 60),
std::make_tuple(u"2 hours ago", 60 * 60 * 2),
std::make_tuple(u"23 hours ago", 60 * 60 * 23),
std::make_tuple(u"yesterday", 60 * 60 * 24),
std::make_tuple(u"yesterday", 60 * 60 * 24 * 2 - 1),
std::make_tuple(u"2 days ago", 60 * 60 * 24 * 2),
std::make_tuple(u"2 days ago", 60 * 60 * 24 * 3 - 1),
std::make_tuple(u"3 days ago", 60 * 60 * 24 * 3),
std::make_tuple(u"3 days ago", 60 * 60 * 24 * 4 - 1)};
// Test that above time deltas produce the corresponding display strings.
for (auto tuple : tuples) {
const base::Time time = system_time - base::Seconds(std::get<1>(tuple));
const std::u16string display_string =
safety_check_->GetStringForParentRan(time, system_time);
EXPECT_EQ(base::StrCat({u"Safety check ran ", std::get<0>(tuple)}),
display_string);
}
}
TEST_F(SafetyCheckHandlerTest, CheckSafetyCheckStartedWebUiEvents) {
safety_check_->SendSafetyCheckStartedWebUiUpdates();
// Check that all initial updates ("running" states) are sent.
const base::Value::Dict* event_parent =
GetSafetyCheckStatusChangedWithDataIfExists(
kParent,
static_cast<int>(SafetyCheckHandler::ParentStatus::kChecking));
ASSERT_TRUE(event_parent);
VerifyDisplayString(event_parent, u"Running…");
const base::Value::Dict* event_updates =
GetSafetyCheckStatusChangedWithDataIfExists(
kUpdates,
static_cast<int>(SafetyCheckHandler::UpdateStatus::kChecking));
ASSERT_TRUE(event_updates);
VerifyDisplayString(event_updates, u"");
const base::Value::Dict* event_pws =
GetSafetyCheckStatusChangedWithDataIfExists(
kPasswords,
static_cast<int>(SafetyCheckHandler::PasswordsStatus::kChecking));
ASSERT_TRUE(event_pws);
VerifyDisplayString(event_pws, u"");
const base::Value::Dict* event_sb =
GetSafetyCheckStatusChangedWithDataIfExists(
kSafeBrowsing,
static_cast<int>(SafetyCheckHandler::SafeBrowsingStatus::kChecking));
ASSERT_TRUE(event_sb);
VerifyDisplayString(event_sb, u"");
const base::Value::Dict* event_extensions =
GetSafetyCheckStatusChangedWithDataIfExists(
kExtensions,
static_cast<int>(SafetyCheckHandler::ExtensionsStatus::kChecking));
ASSERT_TRUE(event_extensions);
VerifyDisplayString(event_extensions, u"");
}
TEST_F(SafetyCheckHandlerTest, CheckSafetyCheckCompletedWebUiEvents) {
// Mock safety check invocation.
safety_check_->PerformSafetyCheck();
// Set the password check mock response.
test_leak_service_->set_state_and_notify(
password_manager::BulkLeakCheckService::State::kSignedOut);
// Check that the parent update is sent after all children checks completed.
const base::Value::Dict* event_parent =
GetSafetyCheckStatusChangedWithDataIfExists(
kParent, static_cast<int>(SafetyCheckHandler::ParentStatus::kAfter));
ASSERT_TRUE(event_parent);
VerifyDisplayString(event_parent, u"Safety check ran a moment ago");
// Check that there is no new parent completion event.
const base::Value::Dict* event_parent2 =
GetSafetyCheckStatusChangedWithDataIfExists(
kParent, static_cast<int>(SafetyCheckHandler::ParentStatus::kAfter));
ASSERT_TRUE(event_parent2);
ASSERT_TRUE(event_parent == event_parent2);
}