| // 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 <optional> |
| #include <utility> |
| |
| #include "base/callback_list.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/run_until.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_future.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/chrome_password_manager_client.h" |
| #include "chrome/browser/password_manager/password_change/change_password_form_finder.h" |
| #include "chrome/browser/password_manager/password_change/login_state_checker.h" |
| #include "chrome/browser/password_manager/password_change/model_quality_logs_uploader.h" |
| #include "chrome/browser/password_manager/password_change/password_change_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/autofill/chrome_autofill_client.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/passwords/bubble_controllers/password_bubble_controller_base.h" |
| #include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h" |
| #include "chrome/browser/ui/passwords/password_change_ui_controller.h" |
| #include "chrome/browser/ui/passwords/ui_utils.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/views/passwords/password_bubble_view_base.h" |
| #include "chrome/browser/ui/views/passwords/password_change/password_change_toast.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/autofill/content/browser/content_autofill_driver.h" |
| #include "components/autofill/content/browser/test_autofill_manager_injector.h" |
| #include "components/autofill/core/browser/foundations/browser_autofill_manager.h" |
| #include "components/autofill/core/browser/foundations/test_autofill_manager_waiter.h" |
| #include "components/autofill/core/common/autofill_test_utils.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/features/password_features.h" |
| #include "components/password_manager/core/browser/one_time_passwords/otp_form_manager.h" |
| #include "components/password_manager/core/browser/one_time_passwords/otp_manager.h" |
| #include "components/password_manager/core/browser/password_form_manager.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" |
| #include "ui/events/test/test_event.h" |
| #include "ui/views/controls/button/image_button.h" |
| #include "ui/views/test/button_test_api.h" |
| |
| namespace { |
| |
| using ::affiliations::MockAffiliationService; |
| using ::base::test::RunOnceCallback; |
| using ::base::test::RunOnceCallbackRepeatedly; |
| using ::optimization_guide::TestModelQualityLogsUploaderService; |
| using ::testing::_; |
| using ::testing::An; |
| using ::testing::DoAll; |
| using ::testing::NiceMock; |
| using ::testing::Return; |
| using ::testing::SizeIs; |
| using ::testing::WithArg; |
| using FinalModelStatus = ::optimization_guide::proto::FinalModelStatus; |
| using OptimizationGuideModelExecutionError = ::optimization_guide:: |
| OptimizationGuideModelExecutionError::ModelExecutionError; |
| using PasswordChangeErrorCase = ::optimization_guide::proto:: |
| PasswordChangeSubmissionData_PasswordChangeErrorCase; |
| using PasswordChangeOutcome = ::optimization_guide::proto:: |
| PasswordChangeSubmissionData_PasswordChangeOutcome; |
| using OpenFormResponseData = ::optimization_guide::proto::OpenFormResponseData; |
| using QualityStatus = ::optimization_guide::proto:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus; |
| using SubmissionOutcome = PasswordChangeSubmissionVerifier::SubmissionOutcome; |
| using SubmitFormResponseData = |
| ::optimization_guide::proto::SubmitFormResponseData; |
| |
| constexpr char kPasswordChangeSubmissionOutcomeHistogram[] = |
| "PasswordManager.PasswordChangeSubmissionOutcome"; |
| constexpr char kMainHost[] = "example.com"; |
| constexpr char kDifferentHost[] = "foo.com"; |
| constexpr char kChangePasswordURL[] = "https://example.com/password/"; |
| |
| class MockPasswordChangeDelegateObserver |
| : public PasswordChangeDelegate::Observer { |
| public: |
| MOCK_METHOD(void, |
| OnStateChanged, |
| (PasswordChangeDelegate::State), |
| (override)); |
| MOCK_METHOD(void, |
| OnPasswordChangeStopped, |
| (PasswordChangeDelegate*), |
| (override)); |
| }; |
| |
| class TestAutofillManager : public autofill::BrowserAutofillManager { |
| public: |
| explicit TestAutofillManager(autofill::ContentAutofillDriver* driver) |
| : BrowserAutofillManager(driver) {} |
| |
| testing::AssertionResult WaitForFormsSeen(int min_num_awaited_calls) { |
| return forms_seen_waiter_.Wait(min_num_awaited_calls); |
| } |
| |
| private: |
| autofill::TestAutofillManagerWaiter forms_seen_waiter_{ |
| *this, |
| {autofill::AutofillManagerEvent::kFormsSeen}}; |
| }; |
| |
| std::unique_ptr<KeyedService> CreateTestAffiliationService( |
| content::BrowserContext* context) { |
| return std::make_unique<NiceMock<MockAffiliationService>>(); |
| } |
| |
| std::unique_ptr<KeyedService> CreateOptimizationService( |
| content::BrowserContext* context) { |
| return std::make_unique<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_THAT(ukm_entries, SizeIs(1)); |
| return ukm_entries[0]; |
| } |
| |
| } // namespace |
| |
| class PasswordChangeBrowserTest : public PasswordManagerBrowserTestBase { |
| public: |
| PasswordChangeBrowserTest() { |
| // TODO (crbug.com/439496997): Fix the test to work with this feature flag |
| // default value. |
| scoped_feature_list_.InitWithFeatures( |
| {password_manager::features::kSubmitWithEnterDuringPasswordChange}, |
| {password_manager::features::kCheckLoginStateBeforePasswordChange}); |
| } |
| |
| 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(QualityStatus open_form_status, |
| QualityStatus submit_form_status, |
| QualityStatus verify_submission_status, |
| FinalModelStatus final_status) { |
| const auto& logs = logs_uploader().uploaded_logs(); |
| ASSERT_EQ(1, std::ranges::count_if(logs, [](const auto& log) { |
| return log->password_change_submission().has_quality(); |
| })); |
| const auto it = std::find_if(logs.begin(), logs.end(), [](const auto& log) { |
| return log->password_change_submission().has_quality(); |
| }); |
| // Verify the single log values. |
| optimization_guide::proto::PasswordChangeQuality quality = |
| it->get()->password_change_submission().quality(); |
| EXPECT_EQ(quality.final_model_status(), final_status); |
| EXPECT_EQ(quality.verify_submission().status(), verify_submission_status); |
| EXPECT_EQ(quality.open_form().status(), open_form_status); |
| EXPECT_EQ(quality.submit_form().status(), submit_form_status); |
| } |
| |
| void SetPrivacyNoticeAcceptedPref() { |
| ON_CALL(*mock_optimization_guide_keyed_service(), |
| ShouldFeatureBeCurrentlyEnabledForUser( |
| optimization_guide::UserVisibleFeatureKey:: |
| kPasswordChangeSubmission)) |
| .WillByDefault(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()); |
| } |
| |
| ChromePasswordManagerClient* client() const { |
| return ChromePasswordManagerClient::FromWebContents(WebContents()); |
| } |
| |
| TestAutofillManager* GetAutofillManager() { |
| return autofill_manager_injector_[WebContents()->GetPrimaryMainFrame()]; |
| } |
| |
| void SetModelQualityLogsUploader() { |
| 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)); |
| } |
| |
| 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()); |
| }), |
| WithArg<3>([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))); |
| }))); |
| } |
| |
| autofill::FormData CreateSimpleOtp() { |
| content::RenderFrameHost* rfh = WebContents()->GetPrimaryMainFrame(); |
| autofill::LocalFrameToken frame_token(rfh->GetFrameToken().value()); |
| autofill::FormData form; |
| form.set_url(GURL("https://www.foo.com")); |
| form.set_renderer_id(autofill::test::MakeFormRendererId()); |
| autofill::FormFieldData field = {autofill::test::CreateTestFormField( |
| "some_label", "some_name", "some_value", |
| autofill::FormControlType::kInputText)}; |
| |
| form.set_fields({field}); |
| return autofill::test::CreateFormDataForFrame(form, frame_token); |
| } |
| |
| void AddOtpToThePage() { |
| auto form = CreateSimpleOtp(); |
| |
| GetAutofillManager()->OnFormsSeen( |
| /*updated_forms=*/{form}, |
| /*removed_forms=*/{}); |
| ASSERT_TRUE(GetAutofillManager()->WaitForFormsSeen(1)); |
| ASSERT_TRUE( |
| GetAutofillManager()->FindCachedFormById(form.fields()[0].global_id())); |
| |
| password_manager::OtpManager* otp_manager = |
| ChromePasswordManagerClient::FromWebContents(WebContents()) |
| ->GetOtpManager(); |
| otp_manager->ProcessClassificationModelPredictions( |
| form, {{form.fields()[0].global_id(), autofill::ONE_TIME_CODE}}); |
| } |
| |
| private: |
| autofill::test::AutofillUnitTestEnvironment autofill_environment_{ |
| {.disable_server_communication = true}}; |
| base::CallbackListSubscription create_services_subscription_; |
| autofill::TestAutofillManagerInjector<TestAutofillManager> |
| autofill_manager_injector_; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| base::WeakPtrFactory<PasswordChangeBrowserTest> weak_ptr_factory_{this}; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, |
| PasswordChangeDoesNotStartUntilPrivacyNoticeAccepted) { |
| base::HistogramTester histogram_tester; |
| TabStripModel* tab_strip = browser()->tab_strip_model(); |
| // Assert that there is a single tab. |
| ASSERT_EQ(tab_strip->count(), 1); |
| ASSERT_FALSE( |
| password_change_service()->GetPasswordChangeDelegate(WebContents())); |
| |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(GURL(kChangePasswordURL))); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"password", WebContents()); |
| auto* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| |
| // Verify password change didn't start yet. |
| EXPECT_FALSE(static_cast<PasswordChangeDelegateImpl*>(delegate)->executor()); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kWaitingForAgreement); |
| |
| // Privacy notice accepted. |
| delegate->OnPrivacyNoticeAccepted(); |
| |
| // Verify a new web_contents is created. |
| auto* web_contents = |
| static_cast<PasswordChangeDelegateImpl*>(delegate)->executor(); |
| EXPECT_TRUE(web_contents); |
| // Verify a new web_contents is opened with a change pwd url. |
| EXPECT_EQ(web_contents->GetURL(), GURL(kChangePasswordURL)); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kWaitingForChangePasswordForm); |
| histogram_tester.ExpectTotalCount( |
| "PasswordManager.PasswordChange.LeakDetectionDialog.TimeSpent." |
| "WithPrivacyNotice", |
| 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, |
| ChangePasswordFormIsFilledAutomatically) { |
| SetPrivacyNoticeAcceptedPref(); |
| |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(embedded_test_server()->GetURL( |
| "/password/update_form_empty_fields_no_submit.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| |
| content::WebContents* web_contents = |
| static_cast<PasswordChangeDelegateImpl*>(delegate)->executor(); |
| // Start observing web_contents where password change happens. |
| SetWebContents(web_contents); |
| PasswordsNavigationObserver observer(web_contents); |
| 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(Return(embedded_test_server()->GetURL( |
| "/password/update_form_empty_fields.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| |
| // Verify the delegate is created. |
| base::WeakPtr<PasswordChangeDelegate> delegate = |
| password_change_service() |
| ->GetPasswordChangeDelegate(WebContents()) |
| ->AsWeakPtr(); |
| ASSERT_TRUE(delegate); |
| |
| // Verify delegate is waiting for change password form when password change |
| // starts. |
| delegate->AddObserver(&observer); |
| delegate->StartPasswordChangeFlow(); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kWaitingForChangePasswordForm); |
| |
| // Verify observer is invoked when the state changes. |
| EXPECT_CALL(observer, |
| OnStateChanged(PasswordChangeDelegate::State::kChangingPassword)); |
| |
| base::WeakPtr<content::WebContents> web_contents = |
| static_cast<PasswordChangeDelegateImpl*>(delegate.get()) |
| ->executor() |
| ->GetWeakPtr(); |
| // Start observing web_contents where password change happens. |
| SetWebContents(web_contents.get()); |
| PasswordsNavigationObserver navigation_observer(web_contents.get()); |
| EXPECT_TRUE(navigation_observer.Wait()); |
| |
| // Wait and verify the old password is filled correctly. |
| WaitForElementValue("password", "pa$$word"); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kChangingPassword); |
| |
| // Observe original web_contnets again to avoid dangling ptr. |
| SetWebContents(browser()->tab_strip_model()->GetWebContentsAt(0)); |
| 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(Return(embedded_test_server()->GetURL( |
| "/password/update_form_empty_fields_no_submit.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| |
| // Start observing web_contents where password change happens. |
| auto* delegate_impl = static_cast<PasswordChangeDelegateImpl*>(delegate); |
| SetWebContents(delegate_impl->executor()); |
| PasswordsNavigationObserver observer(WebContents()); |
| EXPECT_TRUE(observer.Wait()); |
| WaitForElementValue("password", "pa$$word"); |
| |
| // Verify generated password is pre-saved. |
| WaitForPasswordStore(); |
| std::string generated_password = |
| base::UTF16ToUTF8(delegate_impl->generated_password()); |
| EXPECT_EQ(generated_password, |
| GetElementValue(/*iframe_id=*/"null", "new_password_1")); |
| CheckThatCredentialsStored( |
| /*username=*/"test", "pa$$word", 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(Return(embedded_test_server()->GetURL("/password/done.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| ASSERT_TRUE( |
| password_change_service()->GetPasswordChangeDelegate(WebContents())); |
| |
| password_change_service()->GetPasswordChangeDelegate(WebContents())->Stop(); |
| EXPECT_FALSE( |
| password_change_service()->GetPasswordChangeDelegate(WebContents())); |
| } |
| |
| 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(Return(embedded_test_server()->GetURL( |
| "/password/update_form_empty_fields.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| MockPasswordChangeOutcome( |
| PasswordChangeOutcome:: |
| PasswordChangeSubmissionData_PasswordChangeOutcome_SUCCESSFUL_OUTCOME); |
| |
| EXPECT_TRUE(base::test::RunUntil([delegate]() { |
| return delegate->GetCurrentState() == |
| PasswordChangeDelegate::State::kPasswordSuccessfullyChanged; |
| })); |
| CheckThatCredentialsStored( |
| /*username=*/"test", |
| base::UTF16ToUTF8(static_cast<PasswordChangeDelegateImpl*>(delegate) |
| ->generated_password()), |
| "pa$$word", password_manager::PasswordForm::Type::kChangeSubmission); |
| |
| base::WeakPtr<PasswordChangeDelegate> delegate_weak_ptr = |
| delegate->AsWeakPtr(); |
| delegate_weak_ptr->Stop(); |
| EXPECT_TRUE(base::test::RunUntil([&delegate_weak_ptr]() { |
| // Delegate's destructor is called async, so this is needed before checking |
| // the metrics report. |
| return delegate_weak_ptr == 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.ExpectUniqueSample( |
| "PasswordManager.PasswordChange.UserHasPasswordSavedOnAPCLaunch", false, |
| 1); |
| histogram_tester.ExpectTotalCount( |
| "PasswordManager.ChangePasswordFormDetectionTime", 1); |
| histogram_tester.ExpectTotalCount( |
| "PasswordManager.ChangingPasswordToast.TimeSpent", 1); |
| histogram_tester.ExpectTotalCount( |
| "PasswordManager.PasswordChange.LeakDetectionDialog.TimeSpent." |
| "WithoutPrivacyNotice", |
| 1); |
| ukm::TestUkmRecorder::ExpectEntryMetric( |
| GetMetricEntry( |
| test_ukm_recorder, |
| ukm::builders::PasswordManager_PasswordChangeSubmissionOutcome:: |
| kEntryName), |
| ukm::builders::PasswordManager_PasswordChangeSubmissionOutcome:: |
| kPasswordChangeSubmissionOutcomeName, |
| static_cast<int>(SubmissionOutcome::kSuccess)); |
| VerifyUniqueQualityLog( |
| /*open_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_STEP_SKIPPED, |
| /* submit_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_STEP_SKIPPED, |
| /*verify_submission_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_ACTION_SUCCESS, |
| /*final_status=*/ |
| FinalModelStatus::FINAL_MODEL_STATUS_SUCCESS); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, OldPasswordIsUpdated) { |
| 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(Return(embedded_test_server()->GetURL( |
| kMainHost, "/password/update_form_empty_fields.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(url, u"test", u"pa$$word", |
| WebContents()); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| MockPasswordChangeOutcome( |
| PasswordChangeOutcome:: |
| PasswordChangeSubmissionData_PasswordChangeOutcome_SUCCESSFUL_OUTCOME); |
| |
| 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(static_cast<PasswordChangeDelegateImpl*>(delegate) |
| ->generated_password()), |
| base::UTF16ToUTF8(form.password_value), |
| 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(Return(embedded_test_server()->GetURL( |
| kMainHost, "/password/update_form_empty_fields.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(origin, u"test", u"pa$$word", |
| WebContents()); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| 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)); |
| |
| 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(Return(embedded_test_server()->GetURL( |
| kMainHost, "/password/update_form_empty_fields.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(origin, u"test", u"pa$$word", |
| WebContents()); |
| password_change_service() |
| ->GetPasswordChangeDelegate(WebContents()) |
| ->StartPasswordChangeFlow(); |
| |
| 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(); |
| CheckThatCredentialsStored( |
| /*username=*/"test", "pa$$word", |
| base::UTF16ToUTF8( |
| static_cast<PasswordChangeDelegateImpl*>( |
| password_change_service()->GetPasswordChangeDelegate( |
| WebContents())) |
| ->generated_password())); |
| |
| 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, |
| PasswordChangeSubmissionVerifier::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( |
| /*open_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_STEP_SKIPPED, |
| /* submit_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_STEP_SKIPPED, |
| /*verify_submission_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_FAILURE_STATUS, |
| /*final_status=*/ |
| FinalModelStatus::FINAL_MODEL_STATUS_FAILURE); |
| } |
| |
| 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(Return(change_password_url)); |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| |
| TabStripModel* tab_strip = browser()->tab_strip_model(); |
| ASSERT_EQ(tab_strip->count(), 1); |
| |
| EXPECT_EQ(tab_strip->active_index(), 0); |
| delegate->OpenPasswordChangeTab(); |
| // Stop the flow as this what happens in reality when user chooses to see a |
| // hidden tab. |
| delegate->Stop(); |
| |
| EXPECT_EQ(tab_strip->count(), 2); |
| EXPECT_EQ(tab_strip->active_index(), 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, |
| LeakCheckDialogWithPrivacyNoticeDisplayed) { |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(embedded_test_server()->GetURL( |
| "/password/update_form_empty_fields.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"password", WebContents()); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kWaitingForAgreement); |
| EXPECT_TRUE(static_cast<PasswordChangeDelegateImpl*>(delegate) |
| ->ui_controller() |
| ->dialog_widget() |
| ->IsVisible()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, FailureDialogDisplayed) { |
| SetPrivacyNoticeAcceptedPref(); |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(embedded_test_server()->GetURL( |
| "/password/update_form_empty_fields.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| MockPasswordChangeOutcome( |
| PasswordChangeOutcome:: |
| PasswordChangeSubmissionData_PasswordChangeOutcome_UNSUCCESSFUL_OUTCOME); |
| |
| ASSERT_TRUE(base::test::RunUntil([delegate]() { |
| return delegate->GetCurrentState() == |
| PasswordChangeDelegate::State::kPasswordChangeFailed; |
| })); |
| |
| EXPECT_TRUE(static_cast<PasswordChangeDelegateImpl*>(delegate) |
| ->ui_controller() |
| ->dialog_widget() |
| ->IsVisible()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, |
| LeakCheckDialogWithoutPrivacyNoticeDisplayed) { |
| SetPrivacyNoticeAcceptedPref(); |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(embedded_test_server()->GetURL( |
| "/password/update_form_empty_fields.html"))); |
| |
| 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); |
| EXPECT_TRUE(static_cast<PasswordChangeDelegateImpl*>(delegate) |
| ->ui_controller() |
| ->dialog_widget() |
| ->IsVisible()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, OTPDetectionHaltsTheFlow) { |
| SetPrivacyNoticeAcceptedPref(); |
| autofill::FormData form; |
| autofill::FormFieldData field; |
| field.set_name(u"otp"); |
| field.set_id_attribute(field.name()); |
| field.set_name_attribute(field.name()); |
| field.set_form_control_type(autofill::FormControlType::kInputText); |
| field.set_renderer_id(autofill::FieldRendererId(1)); |
| form.set_fields({field}); |
| password_manager::OtpFormManager otp_form_manager(form, {field.global_id()}, |
| client()); |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(embedded_test_server()->GetURL("/password/done.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| SetModelQualityLogsUploader(); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| ASSERT_TRUE(delegate); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kWaitingForChangePasswordForm); |
| |
| auto* delegate_impl = static_cast<PasswordChangeDelegateImpl*>(delegate); |
| delegate_impl->OnOtpFieldDetected(&otp_form_manager); |
| |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kOtpDetected); |
| EXPECT_TRUE(delegate_impl->ui_controller()->dialog_widget()->IsVisible()); |
| EXPECT_EQ(browser()->tab_strip_model()->count(), 1); |
| delegate_impl->ui_controller()->CallOnDialogCanceledForTesting(); |
| |
| // The quality log is uploaded in the destructor. |
| base::WeakPtr<PasswordChangeDelegate> delegate_weak_ptr = |
| delegate->AsWeakPtr(); |
| EXPECT_TRUE(base::test::RunUntil( |
| [&delegate_weak_ptr]() { return !delegate_weak_ptr; })); |
| |
| VerifyUniqueQualityLog( |
| /*open_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_OTP_DETECTED, |
| /* submit_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_UNKNOWN_STATUS, |
| /*verify_submission_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_UNKNOWN_STATUS, |
| /*final_status=*/ |
| FinalModelStatus::FINAL_MODEL_STATUS_UNSPECIFIED); |
| } |
| |
| // Verify that clicking cancel on the toast, stops the flow |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, CancelFromToast) { |
| SetPrivacyNoticeAcceptedPref(); |
| |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(testing::Return( |
| embedded_test_server()->GetURL("/password/done.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| EXPECT_TRUE(delegate); |
| delegate->StartPasswordChangeFlow(); |
| PasswordChangeUIController* ui_controller = |
| static_cast<PasswordChangeDelegateImpl*>(delegate)->ui_controller(); |
| EXPECT_TRUE(ui_controller->toast_view()); |
| // Verify action button is present and visible. |
| EXPECT_TRUE(ui_controller->toast_view()->action_button()); |
| EXPECT_TRUE(ui_controller->toast_view()->action_button()->GetVisible()); |
| |
| SetModelQualityLogsUploader(); |
| |
| // Click action button, this should cancel the flow. |
| views::test::ButtonTestApi clicker( |
| ui_controller->toast_view()->action_button()); |
| clicker.NotifyClick(ui::test::TestEvent()); |
| |
| EXPECT_EQ(PasswordChangeDelegate::State::kCanceled, |
| delegate->GetCurrentState()); |
| |
| // Verify toast is displayed. |
| EXPECT_TRUE(ui_controller->toast_view()); |
| // Verify the toast has no action button, meaning it's just a |
| // confirmation. |
| EXPECT_FALSE(ui_controller->toast_view()->action_button()->GetVisible()); |
| |
| // The quality log is uploaded in the destructor. |
| base::WeakPtr<PasswordChangeDelegate> delegate_weak_ptr = |
| delegate->AsWeakPtr(); |
| EXPECT_TRUE(base::test::RunUntil( |
| [&delegate_weak_ptr]() { return !delegate_weak_ptr; })); |
| |
| VerifyUniqueQualityLog( |
| /*open_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_FLOW_INTERRUPTED, |
| /* submit_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_UNKNOWN_STATUS, |
| /*verify_submission_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_UNKNOWN_STATUS, |
| /*final_status=*/ |
| FinalModelStatus::FINAL_MODEL_STATUS_UNSPECIFIED); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, |
| ViewDetailsFromToastAfterPageNavigation) { |
| SetPrivacyNoticeAcceptedPref(); |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(testing::Return(embedded_test_server()->GetURL( |
| kMainHost, "/password/update_form_empty_fields.html"))); |
| EXPECT_CALL(*affiliation_service(), GetPSLExtensions) |
| .WillRepeatedly(RunOnceCallbackRepeatedly<0>(std::vector<std::string>())); |
| EXPECT_CALL(*affiliation_service(), GetAffiliationsAndBranding) |
| .WillRepeatedly( |
| RunOnceCallbackRepeatedly<1>(affiliations::AffiliatedFacets(), true)); |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| |
| MockPasswordChangeOutcome( |
| PasswordChangeOutcome:: |
| PasswordChangeSubmissionData_PasswordChangeOutcome_SUCCESSFUL_OUTCOME); |
| |
| EXPECT_TRUE(base::test::RunUntil([delegate]() { |
| return delegate->GetCurrentState() == |
| PasswordChangeDelegate::State::kPasswordSuccessfullyChanged; |
| })); |
| |
| EXPECT_TRUE(delegate); |
| |
| // Navigate to some other website before pressing the button. |
| GURL url = embedded_test_server()->GetURL( |
| kDifferentHost, "/password/update_form_empty_fields.html"); |
| ASSERT_TRUE(content::NavigateToURL(WebContents(), url)); |
| ASSERT_TRUE(content::WaitForLoadStop(WebContents())); |
| |
| auto* toast = static_cast<PasswordChangeDelegateImpl*>(delegate) |
| ->ui_controller() |
| ->toast_view(); |
| EXPECT_TRUE(toast); |
| // Verify action button is present and visible. |
| EXPECT_TRUE(toast->action_button()); |
| EXPECT_TRUE(toast->action_button()->GetVisible()); |
| |
| // Click action button, this should open Password Management. |
| views::test::ButtonTestApi clicker(toast->action_button()); |
| delegate = nullptr; |
| toast = nullptr; |
| |
| TabStripModel* tab_strip = browser()->tab_strip_model(); |
| ASSERT_EQ(1, tab_strip->count()); |
| EXPECT_EQ(0, tab_strip->active_index()); |
| |
| clicker.NotifyClick(ui::test::TestEvent()); |
| |
| EXPECT_EQ(2, tab_strip->count()); |
| EXPECT_EQ(1, tab_strip->active_index()); |
| |
| // Verify Password Management UI is opened. |
| EXPECT_EQ(url::Origin::Create(GURL("chrome://password-manager/")), |
| url::Origin::Create(tab_strip->GetActiveWebContents()->GetURL())); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, ViewPasswordBubbleFromToast) { |
| SetPrivacyNoticeAcceptedPref(); |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(testing::Return(embedded_test_server()->GetURL( |
| kMainHost, "/password/update_form_empty_fields.html"))); |
| EXPECT_CALL(*affiliation_service(), GetPSLExtensions) |
| .WillRepeatedly(RunOnceCallbackRepeatedly<0>(std::vector<std::string>())); |
| EXPECT_CALL(*affiliation_service(), GetAffiliationsAndBranding) |
| .WillRepeatedly( |
| RunOnceCallbackRepeatedly<1>(affiliations::AffiliatedFacets(), true)); |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| |
| MockPasswordChangeOutcome( |
| PasswordChangeOutcome:: |
| PasswordChangeSubmissionData_PasswordChangeOutcome_SUCCESSFUL_OUTCOME); |
| |
| EXPECT_TRUE(base::test::RunUntil([delegate]() { |
| return delegate->GetCurrentState() == |
| PasswordChangeDelegate::State::kPasswordSuccessfullyChanged; |
| })); |
| EXPECT_TRUE(delegate); |
| |
| BubbleObserver prompt_observer(WebContents()); |
| |
| PasswordChangeToast* toast = |
| static_cast<PasswordChangeDelegateImpl*>(delegate) |
| ->ui_controller() |
| ->toast_view(); |
| EXPECT_TRUE(toast); |
| // Verify action button is present and visible. |
| EXPECT_TRUE(toast->action_button()); |
| EXPECT_TRUE(toast->action_button()->GetVisible()); |
| |
| // Click action button, this should open the password bubble. |
| views::test::ButtonTestApi clicker(toast->action_button()); |
| delegate = nullptr; |
| toast = nullptr; |
| |
| clicker.NotifyClick(ui::test::TestEvent()); |
| EXPECT_TRUE(prompt_observer.IsBubbleDisplayedAutomatically()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, |
| ToastHiddenWhenDialogDisplayed) { |
| 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"))); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| MockPasswordChangeOutcome( |
| PasswordChangeOutcome:: |
| PasswordChangeSubmissionData_PasswordChangeOutcome_UNSUCCESSFUL_OUTCOME); |
| |
| ASSERT_TRUE(base::test::RunUntil([delegate]() { |
| return delegate->GetCurrentState() == |
| PasswordChangeDelegate::State::kPasswordChangeFailed; |
| })); |
| |
| PasswordChangeUIController* ui_controller = |
| static_cast<PasswordChangeDelegateImpl*>(delegate)->ui_controller(); |
| EXPECT_TRUE(ui_controller->dialog_widget()->IsVisible()); |
| EXPECT_FALSE(ui_controller->toast_view()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, |
| LogsUserHasPasswordSavedOnAPCLaunch) { |
| password_manager::PasswordFormManager:: |
| set_wait_for_server_predictions_for_filling(false); |
| const GURL url = |
| embedded_test_server()->GetURL(kMainHost, "/password/password_form.html"); |
| password_manager::PasswordForm form; |
| form.signon_realm = url.GetWithEmptyPath().spec(); |
| form.url = url; |
| form.username_value = u"test"; |
| form.password_value = u"pa$$word"; |
| ProfilePasswordStoreFactory::GetForProfile(browser()->profile(), |
| ServiceAccessType::IMPLICIT_ACCESS) |
| ->AddLogin(form); |
| WaitForPasswordStore(); |
| |
| SetPrivacyNoticeAcceptedPref(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(url)) |
| .WillOnce(Return(embedded_test_server()->GetURL( |
| kMainHost, "/password/update_form_empty_fields.html"))); |
| |
| // Navigate to the page again to trigger autofill. |
| PasswordsNavigationObserver observer(WebContents()); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| ASSERT_TRUE(observer.Wait()); |
| |
| base::HistogramTester histogram_tester; |
| password_change_service()->OfferPasswordChangeUi(url, u"test", u"pa$$word", |
| WebContents()); |
| password_change_service() |
| ->GetPasswordChangeDelegate(WebContents()) |
| ->StartPasswordChangeFlow(); |
| histogram_tester.ExpectUniqueSample( |
| "PasswordManager.PasswordChange.UserHasPasswordSavedOnAPCLaunch", true, |
| 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, |
| CrossOriginNavigationDetected) { |
| base::HistogramTester histogram_tester; |
| |
| SetPrivacyNoticeAcceptedPref(); |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(https_test_server().GetURL( |
| kMainHost, "/password/update_form_empty_fields.html"))); |
| EXPECT_CALL(*affiliation_service(), GetPSLExtensions) |
| .WillRepeatedly(RunOnceCallbackRepeatedly<0>(std::vector<std::string>())); |
| EXPECT_CALL(*affiliation_service(), GetAffiliationsAndBranding) |
| .WillRepeatedly( |
| RunOnceCallbackRepeatedly<1>(affiliations::AffiliatedFacets(), true)); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| |
| // Verify the delegate is created. |
| base::WeakPtr<PasswordChangeDelegate> delegate = |
| password_change_service() |
| ->GetPasswordChangeDelegate(WebContents()) |
| ->AsWeakPtr(); |
| ASSERT_TRUE(delegate); |
| |
| // Verify delegate is waiting for change password form when password change |
| // starts. |
| delegate->StartPasswordChangeFlow(); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kWaitingForChangePasswordForm); |
| |
| EXPECT_TRUE(base::test::RunUntil([&delegate]() { |
| return delegate->GetCurrentState() == |
| PasswordChangeDelegate::State::kChangingPassword; |
| })); |
| |
| GURL url = https_test_server().GetURL(kDifferentHost, |
| "/password/simple_password.html"); |
| (void)content::NavigateToURL( |
| static_cast<PasswordChangeDelegateImpl*>(delegate.get())->executor(), |
| url); |
| |
| EXPECT_TRUE(base::test::RunUntil([&delegate]() { |
| return delegate->GetCurrentState() == |
| PasswordChangeDelegate::State::kPasswordChangeFailed; |
| })); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, |
| CrossOriginNavigationDetectedBeforeStartingTheFlow) { |
| SetPrivacyNoticeAcceptedPref(); |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(https_test_server().GetURL( |
| kMainHost, "/password/update_form_empty_fields.html"))); |
| EXPECT_CALL(*affiliation_service(), GetPSLExtensions) |
| .WillOnce(RunOnceCallback<0>(std::vector<std::string>())); |
| EXPECT_CALL(*affiliation_service(), GetAffiliationsAndBranding) |
| .WillOnce(RunOnceCallback<1>(affiliations::AffiliatedFacets(), true)); |
| |
| AddOtpToThePage(); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| |
| // Verify the delegate is created. |
| PasswordChangeDelegateImpl* delegate = |
| static_cast<PasswordChangeDelegateImpl*>( |
| password_change_service()->GetPasswordChangeDelegate(WebContents())); |
| base::WeakPtr<PasswordChangeDelegate> delegate_weak_ptr = |
| delegate->AsWeakPtr(); |
| ASSERT_TRUE(delegate); |
| GURL url = https_test_server().GetURL(kDifferentHost, |
| "/password/simple_password.html"); |
| // Navigate away from the page to a different domain. The flow should be |
| // stopped. |
| ASSERT_TRUE(content::NavigateToURL(WebContents(), url)); |
| |
| EXPECT_TRUE(base::test::RunUntil( |
| [&delegate_weak_ptr]() { return !delegate_weak_ptr; })); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, |
| OnTabCloseLogsUnexpectedFailure) { |
| SetPrivacyNoticeAcceptedPref(); |
| content::WebContents* original_apc_flow_web_contents = WebContents(); |
| int original_apc_flow_tab_index = |
| browser()->tab_strip_model()->GetIndexOfWebContents( |
| original_apc_flow_web_contents); |
| |
| const GURL main_url = original_apc_flow_web_contents->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(embedded_test_server()->GetURL("/password/done.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi( |
| main_url, u"test", u"pa$$word", original_apc_flow_web_contents); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate( |
| original_apc_flow_web_contents); |
| delegate->StartPasswordChangeFlow(); |
| SetModelQualityLogsUploader(); |
| |
| // Set the 'open form' quality log, so that when there is an interruption |
| // the next step is set as FLOW_INTERRUPTED. |
| static_cast<PasswordChangeDelegateImpl*>(delegate) |
| ->logs_uploader() |
| ->SetOpenFormQualityStatus( |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_ACTION_SUCCESS); |
| |
| // Add an extra tab to prevent a dangling pointer when closing |
| // the tab where the main flow is active. |
| std::unique_ptr<content::WebContents> extra_web_contents = |
| content::WebContents::Create( |
| content::WebContents::CreateParams(browser()->profile())); |
| content::WebContents* new_active_web_contents = extra_web_contents.get(); |
| browser()->tab_strip_model()->AppendWebContents(std::move(extra_web_contents), |
| true /* foreground */); |
| SetWebContents(new_active_web_contents); |
| |
| // Close the tab where the flow is active to trigger a |
| // flow interruption log. |
| browser()->tab_strip_model()->CloseWebContentsAt( |
| original_apc_flow_tab_index, TabCloseTypes::CLOSE_USER_GESTURE); |
| |
| base::WeakPtr<PasswordChangeDelegate> delegate_weak_ptr = |
| delegate->AsWeakPtr(); |
| EXPECT_TRUE(base::test::RunUntil( |
| [&delegate_weak_ptr]() { return !delegate_weak_ptr; })); |
| |
| VerifyUniqueQualityLog( |
| /*open_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_ACTION_SUCCESS, |
| /* submit_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_FLOW_INTERRUPTED, |
| /*verify_submission_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_UNKNOWN_STATUS, |
| /*final_status=*/ |
| FinalModelStatus::FINAL_MODEL_STATUS_UNSPECIFIED); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, |
| FlowInterruptedAfterOpenFormStep) { |
| SetPrivacyNoticeAcceptedPref(); |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(embedded_test_server()->GetURL("/password/done.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| SetModelQualityLogsUploader(); |
| |
| // Set the 'open form' quality log, so that when there is an interruption |
| // the next step is set as FLOW_INTERRUPTED. |
| static_cast<PasswordChangeDelegateImpl*>(delegate) |
| ->logs_uploader() |
| ->SetOpenFormQualityStatus( |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_ACTION_SUCCESS); |
| |
| PasswordChangeUIController* ui_controller = |
| static_cast<PasswordChangeDelegateImpl*>(delegate)->ui_controller(); |
| EXPECT_TRUE(base::test::RunUntil( |
| [ui_controller]() { return ui_controller->toast_view(); })); |
| base::WeakPtr<PasswordChangeDelegate> delegate_weak_ptr = |
| delegate->AsWeakPtr(); |
| // Simulate clicking the "cancel" button on the UI toast. |
| views::test::ButtonTestApi clicker( |
| ui_controller->toast_view()->action_button()); |
| clicker.NotifyClick(ui::test::TestEvent()); |
| // Verify that the flow's state is "canceled". |
| EXPECT_EQ(PasswordChangeDelegate::State::kCanceled, |
| delegate->GetCurrentState()); |
| EXPECT_TRUE(base::test::RunUntil( |
| [&delegate_weak_ptr]() { return !delegate_weak_ptr; })); |
| |
| VerifyUniqueQualityLog( |
| /*open_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_ACTION_SUCCESS, |
| /* submit_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_FLOW_INTERRUPTED, |
| /*verify_submission_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_UNKNOWN_STATUS, |
| /*final_status=*/ |
| FinalModelStatus::FINAL_MODEL_STATUS_UNSPECIFIED); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, |
| FlowInterruptedAfterSubmitFormStep) { |
| SetPrivacyNoticeAcceptedPref(); |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(embedded_test_server()->GetURL("/password/done.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| SetModelQualityLogsUploader(); |
| |
| // Set the 'submit form' quality log, so that when there is an interruption |
| // the next step is set as FLOW_INTERRUPTED. |
| static_cast<PasswordChangeDelegateImpl*>(delegate) |
| ->logs_uploader() |
| ->SetSubmitFormQualityStatus( |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_ACTION_SUCCESS); |
| |
| PasswordChangeUIController* ui_controller = |
| static_cast<PasswordChangeDelegateImpl*>(delegate)->ui_controller(); |
| EXPECT_TRUE(base::test::RunUntil( |
| [ui_controller]() { return ui_controller->toast_view(); })); |
| base::WeakPtr<PasswordChangeDelegate> delegate_weak_ptr = |
| delegate->AsWeakPtr(); |
| // Simulate clicking the "cancel" button on the UI toast. |
| views::test::ButtonTestApi clicker( |
| ui_controller->toast_view()->action_button()); |
| clicker.NotifyClick(ui::test::TestEvent()); |
| // Verify that the flow's state is "canceled". |
| EXPECT_EQ(PasswordChangeDelegate::State::kCanceled, |
| delegate->GetCurrentState()); |
| EXPECT_TRUE(base::test::RunUntil( |
| [&delegate_weak_ptr]() { return !delegate_weak_ptr; })); |
| |
| VerifyUniqueQualityLog( |
| /*open_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_UNKNOWN_STATUS, |
| /* submit_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_ACTION_SUCCESS, |
| /*verify_submission_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_FLOW_INTERRUPTED, |
| /*final_status=*/ |
| FinalModelStatus::FINAL_MODEL_STATUS_UNSPECIFIED); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTest, |
| OtpDetectedfterSubmitFormStep) { |
| SetPrivacyNoticeAcceptedPref(); |
| autofill::FormData form; |
| autofill::FormFieldData field; |
| field.set_name(u"otp"); |
| field.set_id_attribute(field.name()); |
| field.set_name_attribute(field.name()); |
| field.set_form_control_type(autofill::FormControlType::kInputText); |
| field.set_renderer_id(autofill::FieldRendererId(1)); |
| form.set_fields({field}); |
| password_manager::OtpFormManager otp_form_manager(form, {field.global_id()}, |
| client()); |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(embedded_test_server()->GetURL("/password/done.html"))); |
| |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"pa$$word", WebContents()); |
| SetModelQualityLogsUploader(); |
| PasswordChangeDelegate* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| |
| // Set the 'submit form' quality log, so that when there is an interruption |
| // the next step is set as FLOW_INTERRUPTED. |
| static_cast<PasswordChangeDelegateImpl*>(delegate) |
| ->logs_uploader() |
| ->SetSubmitFormQualityStatus( |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_ACTION_SUCCESS); |
| base::WeakPtr<PasswordChangeDelegate> delegate_weak_ptr = |
| delegate->AsWeakPtr(); |
| |
| auto* delegate_impl = static_cast<PasswordChangeDelegateImpl*>(delegate); |
| delegate_impl->OnOtpFieldDetected(&otp_form_manager); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kOtpDetected); |
| delegate_impl->ui_controller()->CallOnDialogCanceledForTesting(); |
| EXPECT_TRUE(base::test::RunUntil( |
| [&delegate_weak_ptr]() { return !delegate_weak_ptr; })); |
| |
| VerifyUniqueQualityLog( |
| /*open_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_UNKNOWN_STATUS, |
| /* submit_form_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_ACTION_SUCCESS, |
| /*verify_submission_status=*/ |
| QualityStatus:: |
| PasswordChangeQuality_StepQuality_SubmissionStatus_OTP_DETECTED, |
| /*final_status=*/ |
| FinalModelStatus::FINAL_MODEL_STATUS_UNSPECIFIED); |
| } |
| |
| class PasswordChangeBrowserTestWithLoginCheck |
| : public PasswordChangeBrowserTest { |
| public: |
| PasswordChangeBrowserTestWithLoginCheck() { |
| scoped_feature_list_.InitAndEnableFeature( |
| password_manager::features::kCheckLoginStateBeforePasswordChange); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTestWithLoginCheck, |
| PasswordChangeDoesNotStartUserIsLoggedOut) { |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(GURL(kChangePasswordURL))); |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"password", WebContents()); |
| auto* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| auto* delegate_impl = static_cast<PasswordChangeDelegateImpl*>(delegate); |
| |
| // Verify that the background tab was not created yet. |
| EXPECT_FALSE(delegate_impl->executor()); |
| EXPECT_TRUE(delegate_impl->login_checker()); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kWaitingForChangePasswordForm); |
| |
| delegate_impl->login_checker()->RespondWithLoginStatus(false); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kLoginFormDetected); |
| // Verify that password change fails if the user is not logged in. |
| for (auto i = 1; i < LoginStateChecker::kMaxLoginChecks; i++) { |
| delegate_impl->login_checker()->RespondWithLoginStatus(false); |
| } |
| EXPECT_FALSE(delegate_impl->login_checker()); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kChangePasswordFormNotFound); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTestWithLoginCheck, |
| OpenTabWhenLoggedOut) { |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(GURL(kChangePasswordURL))); |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"password", WebContents()); |
| auto* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| auto* delegate_impl = static_cast<PasswordChangeDelegateImpl*>(delegate); |
| // Verify that the background tab was not created yet. |
| EXPECT_FALSE(delegate_impl->executor()); |
| EXPECT_TRUE(delegate_impl->login_checker()); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kWaitingForChangePasswordForm); |
| |
| delegate_impl->login_checker()->RespondWithLoginStatus(false); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kLoginFormDetected); |
| // Verify that password change fails if the user is not logged in after |
| // maximum amount of attempts. |
| for (auto i = 1; i < LoginStateChecker::kMaxLoginChecks; i++) { |
| delegate_impl->login_checker()->RespondWithLoginStatus(false); |
| } |
| EXPECT_FALSE(delegate_impl->login_checker()); |
| EXPECT_FALSE(delegate_impl->executor()); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kChangePasswordFormNotFound); |
| // When a user is not logged in, we still open a new tab with the |
| // change password URL, so there should be two tabs after. |
| ASSERT_EQ(browser()->tab_strip_model()->count(), 1); |
| delegate->OpenPasswordChangeTab(); |
| ASSERT_EQ(browser()->tab_strip_model()->count(), 2); |
| auto* change_password_contents = |
| browser()->tab_strip_model()->GetWebContentsAt(1); |
| ASSERT_EQ(change_password_contents->GetVisibleURL(), |
| GURL(kChangePasswordURL)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTestWithLoginCheck, |
| PasswordChangeStartsUserIsLoggedIn) { |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(GURL(kChangePasswordURL))); |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"password", WebContents()); |
| auto* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| |
| // Verify that the background tab was not created yet. |
| EXPECT_FALSE(static_cast<PasswordChangeDelegateImpl*>(delegate)->executor()); |
| EXPECT_TRUE( |
| static_cast<PasswordChangeDelegateImpl*>(delegate)->login_checker()); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kWaitingForChangePasswordForm); |
| |
| // Verify that password change continues if the user is logged in. |
| static_cast<PasswordChangeDelegateImpl*>(delegate) |
| ->login_checker() |
| ->RespondWithLoginStatus(true); |
| EXPECT_FALSE( |
| static_cast<PasswordChangeDelegateImpl*>(delegate)->login_checker()); |
| EXPECT_TRUE(static_cast<PasswordChangeDelegateImpl*>(delegate)->executor()); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kWaitingForChangePasswordForm); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PasswordChangeBrowserTestWithLoginCheck, |
| UserSkipsLoginCheck) { |
| const GURL main_url = WebContents()->GetLastCommittedURL(); |
| EXPECT_CALL(*affiliation_service(), GetChangePasswordURL(main_url)) |
| .WillOnce(Return(GURL(kChangePasswordURL))); |
| password_change_service()->OfferPasswordChangeUi(main_url, u"test", |
| u"password", WebContents()); |
| auto* delegate = |
| password_change_service()->GetPasswordChangeDelegate(WebContents()); |
| delegate->StartPasswordChangeFlow(); |
| auto* delegate_impl = static_cast<PasswordChangeDelegateImpl*>(delegate); |
| // Verify that the background tab was not created yet. |
| EXPECT_FALSE(delegate_impl->executor()); |
| EXPECT_TRUE(delegate_impl->login_checker()); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kWaitingForChangePasswordForm); |
| |
| delegate_impl->login_checker()->RespondWithLoginStatus(false); |
| EXPECT_TRUE(delegate_impl->login_checker()); |
| EXPECT_FALSE(delegate_impl->executor()); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kLoginFormDetected); |
| |
| // Failing for the second time changes the state to give an option to |
| // continue. |
| delegate_impl->login_checker()->RespondWithLoginStatus(false); |
| EXPECT_TRUE(delegate_impl->login_checker()); |
| EXPECT_FALSE(delegate_impl->executor()); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kLoginFormDetectedUserCanContinue); |
| |
| // User stays in `kLoginFormDetectedUserCanContinue` after subsequent |
| // failures. |
| delegate_impl->login_checker()->RespondWithLoginStatus(false); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kLoginFormDetectedUserCanContinue); |
| |
| // Now the user clicks "Continue" which skips login check. |
| delegate->ProceedToChangePassword(); |
| EXPECT_FALSE(delegate_impl->login_checker()); |
| EXPECT_TRUE(delegate_impl->executor()); |
| EXPECT_EQ(delegate->GetCurrentState(), |
| PasswordChangeDelegate::State::kWaitingForChangePasswordForm); |
| } |