blob: 62b08d11fc033d21cf7c02865d557989168c1a1b [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <utility>
#include "base/callback_list.h"
#include "base/memory/weak_ptr.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/run_until.h"
#include "chrome/browser/affiliations/affiliation_service_factory.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/optimization_guide/mock_optimization_guide_keyed_service.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
#include "chrome/browser/password_manager/chrome_password_change_service.h"
#include "chrome/browser/password_manager/password_change/change_form_submission_verifier.h"
#include "chrome/browser/password_manager/password_change_delegate.h"
#include "chrome/browser/password_manager/password_change_delegate_impl.h"
#include "chrome/browser/password_manager/password_change_service_factory.h"
#include "chrome/browser/password_manager/password_manager_test_base.h"
#include "chrome/browser/password_manager/passwords_navigation_observer.h"
#include "chrome/browser/password_manager/profile_password_store_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/passwords/bubble_controllers/password_bubble_controller_base.h"
#include "chrome/browser/ui/passwords/bubble_controllers/password_change/password_change_info_bubble_controller.h"
#include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/passwords/password_bubble_view_base.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/affiliations/core/browser/mock_affiliation_service.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/optimization_guide/core/mock_optimization_guide_model_executor.h"
#include "components/optimization_guide/core/model_quality/test_model_quality_logs_uploader_service.h"
#include "components/optimization_guide/core/optimization_guide_features.h"
#include "components/optimization_guide/core/optimization_guide_prefs.h"
#include "components/optimization_guide/core/optimization_guide_proto_util.h"
#include "components/optimization_guide/proto/model_quality_service.pb.h"
#include "components/password_manager/core/browser/password_store/test_password_store.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
#include "components/password_manager/core/common/password_manager_ui.h"
#include "components/prefs/pref_service.h"
#include "components/ukm/test_ukm_recorder.h"
#include "components/url_formatter/elide_url.h"
#include "content/public/test/browser_test.h"
#include "net/dns/mock_host_resolver.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
using affiliations::AffiliationService;
using affiliations::MockAffiliationService;
using PasswordChangeOutcome = ::optimization_guide::proto::
PasswordChangeSubmissionData_PasswordChangeOutcome;
using PasswordChangeErrorCase = ::optimization_guide::proto::
PasswordChangeSubmissionData_PasswordChangeErrorCase;
using OptimizationGuideModelExecutionError = optimization_guide::
OptimizationGuideModelExecutionError::ModelExecutionError;
using ::testing::_;
using ::testing::An;
using ::testing::Contains;
using ::testing::DoAll;
using ::testing::Invoke;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::WithArg;
using SubmissionOutcome = ChangeFormSubmissionVerifier::SubmissionOutcome;
using optimization_guide::TestModelQualityLogsUploaderService;
using FinalModelStatus = optimization_guide::proto::FinalModelStatus;
const char kPasswordChangeSubmissionOutcomeHistogram[] =
"PasswordManager.PasswordChangeSubmissionOutcome";
const char kMainHost[] = "example.com";
const char kChangePasswordURL[] = "https://example.com/password/";
namespace {
class MockPasswordChangeDelegateObserver
: public PasswordChangeDelegate::Observer {
public:
MOCK_METHOD(void,
OnStateChanged,
(PasswordChangeDelegate::State),
(override));
MOCK_METHOD(void,
OnPasswordChangeStopped,
(PasswordChangeDelegate*),
(override));
};
std::unique_ptr<KeyedService> CreateTestAffiliationService(
content::BrowserContext* context) {
return std::make_unique<testing::NiceMock<MockAffiliationService>>();
}
std::unique_ptr<KeyedService> CreateOptimizationService(
content::BrowserContext* context) {
return std::make_unique<
testing::NiceMock<MockOptimizationGuideKeyedService>>();
}
// Verifies that |test_ukm_recorder| recorder has a single entry called |entry|
// and returns it.
const ukm::mojom::UkmEntry* GetMetricEntry(
const ukm::TestUkmRecorder& test_ukm_recorder,
std::string_view entry) {
std::vector<raw_ptr<const ukm::mojom::UkmEntry, VectorExperimental>>
ukm_entries = test_ukm_recorder.GetEntriesByName(entry);
EXPECT_EQ(1u, ukm_entries.size());
return ukm_entries[0];
}
} // namespace
class PasswordChangeBrowserTest : public PasswordManagerBrowserTestBase {
public:
void SetUpInProcessBrowserTestFixture() override {
PasswordManagerBrowserTestBase::SetUpInProcessBrowserTestFixture();
create_services_subscription_ =
BrowserContextDependencyManager::GetInstance()
->RegisterCreateServicesCallbackForTesting(
base::BindRepeating([](content::BrowserContext* context) {
AffiliationServiceFactory::GetInstance()->SetTestingFactory(
context,
base::BindRepeating(&CreateTestAffiliationService));
OptimizationGuideKeyedServiceFactory::GetInstance()
->SetTestingFactory(
context,
base::BindRepeating(&CreateOptimizationService));
}));
}
void SetUpOnMainThread() override {
PasswordManagerBrowserTestBase::SetUpOnMainThread();
// Redirect all requests to localhost.
host_resolver()->AddRule("*", "127.0.0.1");
PasswordsNavigationObserver observer(WebContents());
GURL url = embedded_test_server()->GetURL(kMainHost,
"/password/simple_password.html");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
ASSERT_TRUE(observer.Wait());
}
void VerifyUniqueQualityLog(FinalModelStatus final_status) {
const std::vector<
std::unique_ptr<optimization_guide::proto::LogAiDataRequest>>& logs =
logs_uploader().uploaded_logs();
ASSERT_EQ(1u, logs.size());
EXPECT_EQ(logs[0]
->mutable_password_change_submission()
->mutable_quality()
->final_model_status(),
final_status);
}
void SetPrivacyNoticeAcceptedPref() {
ON_CALL(*mock_optimization_guide_keyed_service(),
ShouldFeatureBeCurrentlyEnabledForUser(
optimization_guide::UserVisibleFeatureKey::
kPasswordChangeSubmission))
.WillByDefault(testing::Return(true));
}
TestModelQualityLogsUploaderService& logs_uploader() {
return *static_cast<TestModelQualityLogsUploaderService*>(
mock_optimization_guide_keyed_service()
->GetModelQualityLogsUploaderService());
}
MockAffiliationService* affiliation_service() {
return static_cast<MockAffiliationService*>(
AffiliationServiceFactory::GetForProfile(browser()->profile()));
}
MockOptimizationGuideKeyedService* mock_optimization_guide_keyed_service() {
return static_cast<MockOptimizationGuideKeyedService*>(
OptimizationGuideKeyedServiceFactory::GetForProfile(
browser()->profile()));
}
ChromePasswordChangeService* password_change_service() {
return PasswordChangeServiceFactory::GetForProfile(browser()->profile());
}
void MockPasswordChangeOutcome(
std::optional<PasswordChangeOutcome> outcome,
std::optional<PasswordChangeErrorCase> error_case = std::nullopt) {
optimization_guide::proto::PasswordChangeResponse response;
response.mutable_outcome_data()->set_submission_outcome(outcome.value());
if (error_case.has_value()) {
response.mutable_outcome_data()->add_error_case(error_case.value());
}
MockOptimizationGuideKeyedService* optimization_service =
mock_optimization_guide_keyed_service();
auto logs_uploader = std::make_unique<TestModelQualityLogsUploaderService>(
g_browser_process->local_state());
auto logs_uploader_weak_ptr = logs_uploader->GetWeakPtr();
optimization_service->SetModelQualityLogsUploaderServiceForTesting(
std::move(logs_uploader));
EXPECT_CALL(*optimization_service,
ExecuteModel(optimization_guide::ModelBasedCapabilityKey::
kPasswordChangeSubmission,
_, _, _))
.WillOnce(DoAll(
WithArg<1>([&](const google::protobuf::MessageLite& request) {
auto& password_change_request = static_cast<
const optimization_guide::proto::PasswordChangeRequest&>(
request);
ASSERT_TRUE(password_change_request.page_context()
.has_annotated_page_content());
ASSERT_TRUE(
password_change_request.page_context().has_ax_tree_data());
}),
WithArg<3>(Invoke([response,
logs_uploader_weak_ptr](auto callback) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
std::move(callback),
optimization_guide::OptimizationGuideModelExecutionResult(
optimization_guide::AnyWrapProto(response),
/*execution_info=*/nullptr),
std::make_unique<
optimization_guide::ModelQualityLogEntry>(
logs_uploader_weak_ptr)));
}))));
}
void CheckPasswordsSavedOnFailure(const std::string& username,
const std::string& new_password) {
scoped_refptr<password_manager::TestPasswordStore> password_store =
static_cast<password_manager::TestPasswordStore*>(
ProfilePasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get());
const std::vector<password_manager::PasswordForm>& passwords_vector =
password_store->stored_passwords().begin()->second;
// Check if |username| + |new password| is stored
bool found_username_with_new_password = false;
// Check if |empty username| + |new password| is stored
bool found_empty_username_with_new_password = false;
for (const auto& form : passwords_vector) {
if (form.username_value == base::ASCIIToUTF16(username) &&
form.password_value == base::ASCIIToUTF16(new_password)) {
found_username_with_new_password = true;
} else if (form.username_value.empty() &&
form.password_value == base::ASCIIToUTF16(new_password)) {
found_empty_username_with_new_password = true;
}
}
EXPECT_FALSE(found_username_with_new_password);
EXPECT_TRUE(found_empty_username_with_new_password);
}
void StartPasswordChange(const GURL& url,
const std::u16string& username,
const std::u16string& password,
content::WebContents* web_contents) {
password_change_service()->OfferPasswordChangeUi(url, username, password,
web_contents);
// Close the leak detection bubble and simulate that it was accepted.
PasswordBubbleViewBase::CloseCurrentBubble();
password_change_service()
->GetPasswordChangeDelegate(web_contents)
->StartPasswordChangeFlow();
}
private:
base::CallbackListSubscription create_services_subscription_;
base::WeakPtrFactory<PasswordChangeBrowserTest> weak_ptr_factory_{this};
};
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
StartingPasswordChangeOpensNewTab) {
SetPrivacyNoticeAcceptedPref();
TabStripModel* tab_strip = browser()->tab_strip_model();
// Assert that there is a single tab.
ASSERT_EQ(1, tab_strip->count());
ASSERT_FALSE(
password_change_service()->GetPasswordChangeDelegate(WebContents()));
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(GURL(kChangePasswordURL)));
StartPasswordChange(main_url, u"test", u"password", WebContents());
// Verify a new tab is added, although the focus remained on the initial tab.
ASSERT_EQ(2, tab_strip->count());
EXPECT_EQ(0, tab_strip->active_index());
// Verify a new tab is opened with a change pwd url.
EXPECT_EQ(kChangePasswordURL, tab_strip->GetWebContentsAt(1)->GetURL());
// Verify that GetPasswordChangeDelegate() returns delegate for both tabs.
EXPECT_TRUE(password_change_service()->GetPasswordChangeDelegate(
tab_strip->GetWebContentsAt(0)));
EXPECT_TRUE(password_change_service()->GetPasswordChangeDelegate(
tab_strip->GetWebContentsAt(1)));
EXPECT_EQ(password_change_service()->GetPasswordChangeDelegate(
tab_strip->GetWebContentsAt(0)),
password_change_service()->GetPasswordChangeDelegate(
tab_strip->GetWebContentsAt(1)));
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
DoesNotOpenNewTabUntilPrivacyNoticeAccepted) {
TabStripModel* tab_strip = browser()->tab_strip_model();
// Assert that there is a single tab.
ASSERT_EQ(1, tab_strip->count());
ASSERT_FALSE(
password_change_service()->GetPasswordChangeDelegate(WebContents()));
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(GURL(kChangePasswordURL)));
StartPasswordChange(main_url, u"test", u"password", WebContents());
// No new tab should be opened until the privacy notice acceptance.
ASSERT_EQ(1, tab_strip->count());
auto* delegate = password_change_service()->GetPasswordChangeDelegate(
browser()->tab_strip_model()->GetWebContentsAt(0));
EXPECT_EQ(delegate->GetCurrentState(),
PasswordChangeDelegate::State::kWaitingForAgreement);
// Privacy notice accepted.
delegate->OnPrivacyNoticeAccepted();
// Verify a new tab is added, although the focus remained on the initial tab.
ASSERT_EQ(2, tab_strip->count());
// Verify a new tab is opened with a change pwd url.
EXPECT_EQ(GURL(kChangePasswordURL), tab_strip->GetWebContentsAt(1)->GetURL());
EXPECT_EQ(delegate->GetCurrentState(),
PasswordChangeDelegate::State::kWaitingForChangePasswordForm);
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
ChangePasswordFormIsFilledAutomatically) {
SetPrivacyNoticeAcceptedPref();
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
"/password/update_form_empty_fields_no_submit.html")));
StartPasswordChange(main_url, u"test", u"pa$$word", WebContents());
// Activate tab with password change to simplify testing.
SetWebContents(browser()->tab_strip_model()->GetWebContentsAt(1));
PasswordsNavigationObserver observer(WebContents());
EXPECT_TRUE(observer.Wait());
// Wait and verify the old password is filled correctly.
WaitForElementValue("password", "pa$$word");
// Verify there is a new password generated and it's filled into both fields.
std::string new_password =
GetElementValue(/*iframe_id=*/"null", "new_password_1");
EXPECT_FALSE(new_password.empty());
CheckElementValue("new_password_2", new_password);
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, PasswordChangeStateUpdated) {
base::HistogramTester histogram_tester;
MockPasswordChangeDelegateObserver observer;
SetPrivacyNoticeAcceptedPref();
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
"/password/update_form_empty_fields.html")));
StartPasswordChange(main_url, u"test", u"pa$$word", WebContents());
// Verify the delegate is created and it's currently waiting for change
// password form.
base::WeakPtr<PasswordChangeDelegate> delegate =
password_change_service()
->GetPasswordChangeDelegate(
browser()->tab_strip_model()->GetWebContentsAt(0))
->AsWeakPtr();
ASSERT_TRUE(delegate);
delegate->AddObserver(&observer);
EXPECT_EQ(PasswordChangeDelegate::State::kWaitingForChangePasswordForm,
delegate->GetCurrentState());
// Verify observer is invoked when the state changes.
EXPECT_CALL(observer,
OnStateChanged(PasswordChangeDelegate::State::kChangingPassword));
// Activate tab with password change to simplify testing.
SetWebContents(browser()->tab_strip_model()->GetWebContentsAt(1));
PasswordsNavigationObserver navigation_observer(WebContents());
EXPECT_TRUE(navigation_observer.Wait());
// Wait and verify the old password is filled correctly.
WaitForElementValue("password", "pa$$word");
EXPECT_EQ(PasswordChangeDelegate::State::kChangingPassword,
delegate->GetCurrentState());
delegate->RemoveObserver(&observer);
delegate->Stop();
EXPECT_TRUE(base::test::RunUntil([&delegate]() {
// Delegate's destructor is called async, so this is needed before checking
// the metrics report.
return delegate == nullptr;
}));
histogram_tester.ExpectUniqueSample(
PasswordChangeDelegateImpl::kFinalPasswordChangeStatusHistogram,
PasswordChangeDelegate::State::kChangingPassword, 1);
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, GeneratedPasswordIsPreSaved) {
SetPrivacyNoticeAcceptedPref();
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
"/password/update_form_empty_fields_no_submit.html")));
StartPasswordChange(main_url, u"test", u"pa$$word", WebContents());
// Activate tab with password change to simplify testing.
SetWebContents(browser()->tab_strip_model()->GetWebContentsAt(1));
PasswordsNavigationObserver observer(WebContents());
EXPECT_TRUE(observer.Wait());
WaitForElementValue("password", "pa$$word");
// Verify generated password is pre-saved.
WaitForPasswordStore();
auto generated_password = password_change_service()
->GetPasswordChangeDelegate(WebContents())
->GetGeneratedPassword();
EXPECT_EQ(base::UTF16ToUTF8(generated_password),
GetElementValue(/*iframe_id=*/"null", "new_password_1"));
CheckThatCredentialsStored(
/*username=*/"", base::UTF16ToUTF8(generated_password));
}
// Verify that after password change is stopped, password change delegate is not
// returned.
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, StopPasswordChange) {
SetPrivacyNoticeAcceptedPref();
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(
embedded_test_server()->GetURL("/password/done.html")));
StartPasswordChange(main_url, u"test", u"pa$$word", WebContents());
auto* password_change_tab = browser()->tab_strip_model()->GetWebContentsAt(1);
ASSERT_TRUE(password_change_service()->GetPasswordChangeDelegate(
password_change_tab));
password_change_service()
->GetPasswordChangeDelegate(password_change_tab)
->Stop();
EXPECT_FALSE(password_change_service()->GetPasswordChangeDelegate(
password_change_tab));
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, NewPasswordIsSaved) {
base::HistogramTester histogram_tester;
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
SetPrivacyNoticeAcceptedPref();
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
"/password/update_form_empty_fields.html")));
StartPasswordChange(main_url, u"test", u"pa$$word", WebContents());
MockPasswordChangeOutcome(
PasswordChangeOutcome::
PasswordChangeSubmissionData_PasswordChangeOutcome_SUCCESSFUL_OUTCOME);
base::WeakPtr<PasswordChangeDelegate> delegate =
password_change_service()
->GetPasswordChangeDelegate(WebContents())
->AsWeakPtr();
EXPECT_TRUE(base::test::RunUntil([delegate]() {
return delegate->GetCurrentState() ==
PasswordChangeDelegate::State::kPasswordSuccessfullyChanged;
}));
CheckThatCredentialsStored(
/*username=*/"test", base::UTF16ToUTF8(delegate->GetGeneratedPassword()),
password_manager::PasswordForm::Type::kChangeSubmission);
delegate->Stop();
EXPECT_TRUE(base::test::RunUntil([&delegate]() {
// Delegate's destructor is called async, so this is needed before checking
// the metrics report.
return delegate == nullptr;
}));
histogram_tester.ExpectUniqueSample(
PasswordChangeDelegateImpl::kFinalPasswordChangeStatusHistogram,
PasswordChangeDelegate::State::kPasswordSuccessfullyChanged, 1);
histogram_tester.ExpectUniqueSample(kPasswordChangeSubmissionOutcomeHistogram,
SubmissionOutcome::kSuccess, 1);
histogram_tester.ExpectTotalCount("PasswordManager.PasswordChangeTimeOverall",
1);
histogram_tester.ExpectUniqueSample(
"PasswordManager.ChangePasswordFormDetected", true, 1);
histogram_tester.ExpectTotalCount(
"PasswordManager.ChangePasswordFormDetectionTime", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(
GetMetricEntry(
test_ukm_recorder,
ukm::builders::PasswordManager_PasswordChangeSubmissionOutcome::
kEntryName),
ukm::builders::PasswordManager_PasswordChangeSubmissionOutcome::
kPasswordChangeSubmissionOutcomeName,
static_cast<int>(SubmissionOutcome::kSuccess));
VerifyUniqueQualityLog(FinalModelStatus::FINAL_MODEL_STATUS_SUCCESS);
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, OldPasswordIsUpdated) {
base::HistogramTester histograms;
SetPrivacyNoticeAcceptedPref();
password_manager::PasswordStoreInterface* password_store =
ProfilePasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get();
const GURL url = WebContents()->GetLastCommittedURL();
password_manager::PasswordForm form;
form.signon_realm = url.GetWithEmptyPath().spec();
form.url = url;
form.username_value = u"test";
form.password_value = u"pa$$word";
password_store->AddLogin(form);
WaitForPasswordStore();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(url))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
kMainHost, "/password/update_form_empty_fields.html")));
StartPasswordChange(url, form.username_value, form.password_value,
WebContents());
MockPasswordChangeOutcome(
PasswordChangeOutcome::
PasswordChangeSubmissionData_PasswordChangeOutcome_SUCCESSFUL_OUTCOME);
PasswordChangeDelegate* delegate =
password_change_service()->GetPasswordChangeDelegate(WebContents());
EXPECT_TRUE(base::test::RunUntil([delegate]() {
return delegate->GetCurrentState() ==
PasswordChangeDelegate::State::kPasswordSuccessfullyChanged;
}));
// Verify saved password is updated.
WaitForPasswordStore();
CheckThatCredentialsStored(
base::UTF16ToUTF8(form.username_value),
base::UTF16ToUTF8(delegate->GetGeneratedPassword()),
password_manager::PasswordForm::Type::kChangeSubmission);
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
PasswordChangeSubmissionFailedEmptyResponse) {
base::HistogramTester histograms;
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
SetPrivacyNoticeAcceptedPref();
password_manager::PasswordStoreInterface* password_store =
ProfilePasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get();
const GURL origin = embedded_test_server()->GetURL(kMainHost, "/");
password_manager::PasswordForm form;
form.signon_realm = origin.spec();
form.url = origin;
form.username_value = u"test";
form.password_value = u"pa$$word";
password_store->AddLogin(form);
WaitForPasswordStore();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(origin))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
kMainHost, "/password/update_form_empty_fields.html")));
StartPasswordChange(origin, form.username_value, form.password_value,
WebContents());
EXPECT_CALL(
*mock_optimization_guide_keyed_service(),
ExecuteModel(optimization_guide::ModelBasedCapabilityKey::
kPasswordChangeSubmission,
_, _,
An<optimization_guide::
OptimizationGuideModelExecutionResultCallback>()))
.WillOnce(base::test::RunOnceCallback<3>(
optimization_guide::OptimizationGuideModelExecutionResult(
base::unexpected(
optimization_guide::OptimizationGuideModelExecutionError::
FromModelExecutionError(
OptimizationGuideModelExecutionError::
kGenericFailure)),
/*execution_info=*/nullptr),
/*log_entry=*/nullptr));
PasswordChangeDelegate* delegate =
password_change_service()->GetPasswordChangeDelegate(WebContents());
EXPECT_TRUE(base::test::RunUntil([delegate]() {
return delegate->GetCurrentState() ==
PasswordChangeDelegate::State::kPasswordChangeFailed;
}));
WaitForPasswordStore();
histograms.ExpectUniqueSample(kPasswordChangeSubmissionOutcomeHistogram,
SubmissionOutcome::kNoResponse, 1);
ukm::TestUkmRecorder::ExpectEntryMetric(
GetMetricEntry(
test_ukm_recorder,
ukm::builders::PasswordManager_PasswordChangeSubmissionOutcome::
kEntryName),
ukm::builders::PasswordManager_PasswordChangeSubmissionOutcome::
kPasswordChangeSubmissionOutcomeName,
static_cast<int>(SubmissionOutcome::kNoResponse));
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
PasswordChangeSubmissionFailed) {
base::HistogramTester histogram_tester;
ukm::TestAutoSetUkmRecorder test_ukm_recorder;
SetPrivacyNoticeAcceptedPref();
password_manager::PasswordStoreInterface* password_store =
ProfilePasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get();
const GURL origin = embedded_test_server()->GetURL(kMainHost, "/");
password_manager::PasswordForm form;
form.signon_realm = origin.spec();
form.url = origin;
form.username_value = u"test";
form.password_value = u"pa$$word";
password_store->AddLogin(form);
WaitForPasswordStore();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(origin))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
kMainHost, "/password/update_form_empty_fields.html")));
StartPasswordChange(origin, form.username_value, form.password_value,
WebContents());
MockPasswordChangeOutcome(
PasswordChangeOutcome::
PasswordChangeSubmissionData_PasswordChangeOutcome_UNSUCCESSFUL_OUTCOME,
PasswordChangeErrorCase::
PasswordChangeSubmissionData_PasswordChangeErrorCase_PAGE_ERROR);
base::WeakPtr<PasswordChangeDelegate> delegate =
password_change_service()
->GetPasswordChangeDelegate(WebContents())
->AsWeakPtr();
EXPECT_TRUE(base::test::RunUntil([delegate]() {
return delegate->GetCurrentState() ==
PasswordChangeDelegate::State::kPasswordChangeFailed;
}));
WaitForPasswordStore();
CheckPasswordsSavedOnFailure(
base::UTF16ToUTF8(form.username_value),
base::UTF16ToUTF8(delegate->GetGeneratedPassword()));
delegate->Stop();
EXPECT_TRUE(base::test::RunUntil([&delegate]() {
// Delegate's destructor is called async, so this is needed before checking
// the metrics report.
return delegate == nullptr;
}));
histogram_tester.ExpectUniqueSample(
PasswordChangeDelegateImpl::kFinalPasswordChangeStatusHistogram,
PasswordChangeDelegate::State::kPasswordChangeFailed, 1);
histogram_tester.ExpectUniqueSample(
kPasswordChangeSubmissionOutcomeHistogram,
ChangeFormSubmissionVerifier::SubmissionOutcome::kPageError, 1);
ukm::TestUkmRecorder::ExpectEntryMetric(
GetMetricEntry(
test_ukm_recorder,
ukm::builders::PasswordManager_PasswordChangeSubmissionOutcome::
kEntryName),
ukm::builders::PasswordManager_PasswordChangeSubmissionOutcome::
kPasswordChangeSubmissionOutcomeName,
static_cast<int>(SubmissionOutcome::kPageError));
VerifyUniqueQualityLog(FinalModelStatus::FINAL_MODEL_STATUS_FAILURE);
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
SignInCheckBubbleIsHiddenWhenStateIsUpdated) {
SetPrivacyNoticeAcceptedPref();
const GURL main_url = WebContents()->GetLastCommittedURL();
const GURL change_password_url =
embedded_test_server()->GetURL("/password/update_form_empty_fields.html");
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(change_password_url));
StartPasswordChange(main_url, u"test", u"pa$$word", WebContents());
// Verify the delegate is created and it's currently waiting for change
// password form.
auto* delegate =
password_change_service()->GetPasswordChangeDelegate(WebContents());
ASSERT_TRUE(delegate);
PasswordBubbleViewBase::ShowBubble(
WebContents(), LocationBarBubbleDelegateView::USER_GESTURE);
auto* bubble_controller = static_cast<PasswordChangeInfoBubbleController*>(
PasswordBubbleViewBase::manage_password_bubble()->GetController());
ASSERT_EQ(
bubble_controller->GetTitle(),
l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_UI_SIGN_IN_CHECK_TITLE));
ASSERT_EQ(url_formatter::FormatUrlForSecurityDisplay(change_password_url),
bubble_controller->GetDisplayOrigin());
// Wait until the state is changed from `kWaitingForChangePasswordForm` to any
// other state. The bubble should disappear then.
ASSERT_TRUE(base::test::RunUntil([delegate]() {
return delegate->GetCurrentState() !=
PasswordChangeDelegate::State::kWaitingForChangePasswordForm;
}));
PasswordBubbleViewBase* bubble =
PasswordBubbleViewBase::manage_password_bubble();
ASSERT_FALSE(bubble);
}
#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, OpenTabWithPasswordChange) {
SetPrivacyNoticeAcceptedPref();
const GURL main_url = WebContents()->GetLastCommittedURL();
const GURL change_password_url =
embedded_test_server()->GetURL("/password/update_form_empty_fields.html");
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(change_password_url));
StartPasswordChange(main_url, u"test", u"password", WebContents());
TabStripModel* tab_strip = browser()->tab_strip_model();
ASSERT_EQ(2, tab_strip->count());
EXPECT_EQ(0, tab_strip->active_index());
password_change_service()
->GetPasswordChangeDelegate(WebContents())
->OpenPasswordChangeTab();
EXPECT_EQ(1, tab_strip->active_index());
}
#endif
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
PrivacyNoticeDisplayedAutomatically) {
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
"/password/update_form_empty_fields.html")));
BubbleObserver prompt_observer(WebContents());
StartPasswordChange(main_url, u"test", u"pa$$word", WebContents());
EXPECT_EQ(PasswordChangeDelegate::State::kWaitingForAgreement,
password_change_service()
->GetPasswordChangeDelegate(WebContents())
->GetCurrentState());
EXPECT_TRUE(base::test::RunUntil(
[&]() { return prompt_observer.IsBubbleDisplayedAutomatically(); }));
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
SuccessfulDialogDisplayedAutomatically) {
SetPrivacyNoticeAcceptedPref();
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
"/password/update_form_empty_fields.html")));
BubbleObserver prompt_observer(WebContents());
StartPasswordChange(main_url, u"test", u"pa$$word", WebContents());
MockPasswordChangeOutcome(
PasswordChangeOutcome::
PasswordChangeSubmissionData_PasswordChangeOutcome_SUCCESSFUL_OUTCOME);
PasswordChangeDelegate* delegate =
password_change_service()->GetPasswordChangeDelegate(WebContents());
EXPECT_TRUE(base::test::RunUntil([delegate]() {
return delegate->GetCurrentState() ==
PasswordChangeDelegate::State::kPasswordSuccessfullyChanged;
}));
// Now bubble should automatically appear.
EXPECT_TRUE(prompt_observer.IsBubbleDisplayedAutomatically());
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
FailureDialogDisplayedAutomatically) {
SetPrivacyNoticeAcceptedPref();
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
"/password/update_form_empty_fields.html")));
BubbleObserver prompt_observer(WebContents());
StartPasswordChange(main_url, u"test", u"pa$$word", WebContents());
MockPasswordChangeOutcome(
PasswordChangeOutcome::
PasswordChangeSubmissionData_PasswordChangeOutcome_UNSUCCESSFUL_OUTCOME);
PasswordChangeDelegate* delegate =
password_change_service()->GetPasswordChangeDelegate(WebContents());
ASSERT_TRUE(base::test::RunUntil([delegate]() {
return delegate->GetCurrentState() ==
PasswordChangeDelegate::State::kPasswordChangeFailed;
}));
// Now bubble should automatically appear.
EXPECT_TRUE(prompt_observer.IsBubbleDisplayedAutomatically());
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
LeakCheckBubbleDisplayedAutomatically) {
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
"/password/update_form_empty_fields.html")));
BubbleObserver prompt_observer(WebContents());
password_change_service()->OfferPasswordChangeUi(main_url, u"test",
u"pa$$word", WebContents());
PasswordChangeDelegate* delegate =
password_change_service()->GetPasswordChangeDelegate(WebContents());
EXPECT_EQ(delegate->GetCurrentState(),
PasswordChangeDelegate::State::kOfferingPasswordChange);
// Now bubble should automatically appear.
EXPECT_TRUE(prompt_observer.IsBubbleDisplayedAutomatically());
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
BubbleIsNotDisplayedWhenSwitchedToDifferentTab) {
SetPrivacyNoticeAcceptedPref();
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
"/password/update_form_empty_fields.html")));
// Open new tab.
ui_test_utils::NavigateToURLWithDisposition(
browser(), embedded_test_server()->GetURL("/password/done.html"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_NO_WAIT);
BubbleObserver prompt_observer(
browser()->tab_strip_model()->GetActiveWebContents());
// Start password change in the old tab
StartPasswordChange(main_url, u"test", u"pa$$word", WebContents());
MockPasswordChangeOutcome(
PasswordChangeOutcome::
PasswordChangeSubmissionData_PasswordChangeOutcome_SUCCESSFUL_OUTCOME);
auto* web_contents = browser()->tab_strip_model()->GetWebContentsAt(2);
PasswordsNavigationObserver password_change_page_observer(web_contents);
EXPECT_TRUE(password_change_page_observer.Wait());
PasswordChangeDelegate* delegate =
password_change_service()->GetPasswordChangeDelegate(web_contents);
EXPECT_TRUE(base::test::RunUntil([delegate]() {
return delegate->GetCurrentState() ==
PasswordChangeDelegate::State::kPasswordSuccessfullyChanged;
}));
// Even after password change is finished no bubble is shown.
EXPECT_FALSE(prompt_observer.IsBubbleDisplayedAutomatically());
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
TabWithPasswordChangeClosedAutomatically) {
SetPrivacyNoticeAcceptedPref();
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
"/password/update_form_empty_fields.html")));
StartPasswordChange(main_url, u"test", u"pa$$word", WebContents());
auto* tab_strip = browser()->tab_strip_model();
ASSERT_EQ(2, tab_strip->count());
MockPasswordChangeOutcome(
PasswordChangeOutcome::
PasswordChangeSubmissionData_PasswordChangeOutcome_SUCCESSFUL_OUTCOME);
auto* web_contents = browser()->tab_strip_model()->GetWebContentsAt(1);
PasswordsNavigationObserver password_change_page_observer(web_contents);
EXPECT_TRUE(password_change_page_observer.Wait());
PasswordChangeDelegate* delegate =
password_change_service()->GetPasswordChangeDelegate(WebContents());
EXPECT_TRUE(base::test::RunUntil([delegate]() {
return delegate->GetCurrentState() ==
PasswordChangeDelegate::State::kPasswordSuccessfullyChanged;
}));
// Expect tab password change to be closed.
EXPECT_TRUE(
base::test::RunUntil([&tab_strip]() { return tab_strip->count() == 1; }));
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest,
FocusedTabRemainsOpenedAfterSuccessfulChange) {
SetPrivacyNoticeAcceptedPref();
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
"/password/update_form_empty_fields.html")));
StartPasswordChange(main_url, u"test", u"pa$$word", WebContents());
auto* tab_strip = browser()->tab_strip_model();
ASSERT_EQ(2, tab_strip->count());
tab_strip->ActivateTabAt(1);
MockPasswordChangeOutcome(
PasswordChangeOutcome::
PasswordChangeSubmissionData_PasswordChangeOutcome_SUCCESSFUL_OUTCOME);
base::WeakPtr<PasswordChangeDelegate> delegate =
password_change_service()
->GetPasswordChangeDelegate(WebContents())
->AsWeakPtr();
EXPECT_TRUE(base::test::RunUntil([delegate]() {
return delegate->GetCurrentState() ==
PasswordChangeDelegate::State::kPasswordSuccessfullyChanged;
}));
// Tab is still open.
ASSERT_EQ(2, tab_strip->count());
// Check its state.
delegate->Stop();
ASSERT_TRUE(base::test::RunUntil([&delegate]() { return !delegate; }));
auto* web_contents = browser()->tab_strip_model()->GetWebContentsAt(1);
EXPECT_EQ(
ManagePasswordsUIController::FromWebContents(web_contents)->GetState(),
password_manager::ui::MANAGE_STATE);
}
IN_PROC_BROWSER_TEST_F(
PasswordChangeBrowserTest,
SuccessfulDialogDisplayedAutomaticallyEvenAfterTheCheckIsFinished) {
SetPrivacyNoticeAcceptedPref();
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(embedded_test_server()->GetURL(
"/password/update_form_empty_fields.html")));
StartPasswordChange(main_url, u"test", u"pa$$word", WebContents());
// Activate tab with change password flow.
browser()->tab_strip_model()->ActivateTabAt(1);
MockPasswordChangeOutcome(
PasswordChangeOutcome::
PasswordChangeSubmissionData_PasswordChangeOutcome_SUCCESSFUL_OUTCOME);
PasswordChangeDelegate* delegate =
password_change_service()->GetPasswordChangeDelegate(WebContents());
EXPECT_TRUE(base::test::RunUntil([delegate]() {
return delegate->GetCurrentState() ==
PasswordChangeDelegate::State::kPasswordSuccessfullyChanged;
}));
// Expect the bubble to be visible.
EXPECT_TRUE(PasswordBubbleViewBase::manage_password_bubble());
PasswordBubbleViewBase::CloseCurrentBubble();
// Verify that activating the original tab shows a bubble automatically again.
BubbleObserver prompt_observer(
browser()->tab_strip_model()->GetWebContentsAt(0));
browser()->tab_strip_model()->ActivateTabAt(0);
EXPECT_TRUE(prompt_observer.IsBubbleDisplayedAutomatically());
}
IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, OTPDetectionHaltsTheFlow) {
SetPrivacyNoticeAcceptedPref();
const GURL main_url = WebContents()->GetLastCommittedURL();
EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url))
.WillOnce(testing::Return(
embedded_test_server()->GetURL("/password/done.html")));
StartPasswordChange(main_url, u"test", u"pa$$word", WebContents());
base::WeakPtr<PasswordChangeDelegate> delegate =
password_change_service()
->GetPasswordChangeDelegate(
browser()->tab_strip_model()->GetWebContentsAt(0))
->AsWeakPtr();
ASSERT_TRUE(delegate);
EXPECT_EQ(PasswordChangeDelegate::State::kWaitingForChangePasswordForm,
delegate->GetCurrentState());
BubbleObserver prompt_observer(WebContents());
delegate->OnOtpFieldDetected(
browser()->tab_strip_model()->GetWebContentsAt(1));
EXPECT_EQ(PasswordChangeDelegate::State::kOtpDetected,
delegate->GetCurrentState());
EXPECT_TRUE(prompt_observer.IsBubbleDisplayedAutomatically());
EXPECT_EQ(2, browser()->tab_strip_model()->count());
}