| // 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 "chrome/browser/ui/webid/identity_dialog_controller.h" |
| |
| #include <memory> |
| |
| #include "base/functional/callback_helpers.h" |
| #include "base/run_loop.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/mock_callback.h" |
| #include "base/test/task_environment.h" |
| #include "chrome/browser/ui/webid/account_selection_view.h" |
| #include "chrome/test/base/chrome_render_view_host_test_harness.h" |
| #include "components/optimization_guide/core/mock_optimization_guide_decider.h" |
| #include "components/permissions/permission_request_manager.h" |
| #include "components/permissions/test/mock_permission_prompt_factory.h" |
| #include "components/permissions/test/mock_permission_request.h" |
| #include "components/segmentation_platform/public/constants.h" |
| #include "components/segmentation_platform/public/features.h" |
| #include "components/segmentation_platform/public/result.h" |
| #include "components/segmentation_platform/public/segmentation_platform_service.h" |
| #include "components/segmentation_platform/public/testing/mock_segmentation_platform_service.h" |
| #include "content/public/browser/identity_request_account.h" |
| #include "content/public/browser/identity_request_dialog_controller.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/mojom/webid/federated_auth_request.mojom.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| using testing::_; |
| |
| namespace { |
| |
| const std::vector<content::IdentityRequestDialogDisclosureField> |
| kDefaultPermissions = { |
| content::IdentityRequestDialogDisclosureField::kName, |
| content::IdentityRequestDialogDisclosureField::kEmail, |
| content::IdentityRequestDialogDisclosureField::kPicture}; |
| |
| } // namespace |
| |
| constexpr char kTopFrameEtldPlusOne[] = "top-frame-example.com"; |
| constexpr char kIdpEtldPlusOne[] = "idp-example.com"; |
| constexpr float kPerPageLoadClickthroughRate = 0.1; |
| constexpr float kPerClientClickthroughRate = 0.2; |
| constexpr float kPerImpressionClickthroughRate = 0.3; |
| constexpr float kLikelyToSignin = 0.4; |
| |
| // Mock version of AccountSelectionView for injection during tests. |
| class MockAccountSelectionView : public AccountSelectionView { |
| public: |
| MockAccountSelectionView() : AccountSelectionView(/*delegate=*/nullptr) {} |
| ~MockAccountSelectionView() override = default; |
| |
| MockAccountSelectionView(const MockAccountSelectionView&) = delete; |
| MockAccountSelectionView& operator=(const MockAccountSelectionView&) = delete; |
| |
| MOCK_METHOD( |
| bool, |
| Show, |
| (const content::RelyingPartyData& rp_data, |
| const std::vector<IdentityProviderDataPtr>& identity_provider_data, |
| const std::vector<IdentityRequestAccountPtr>& accounts, |
| Account::SignInMode sign_in_mode, |
| blink::mojom::RpMode rp_mode, |
| const std::vector<IdentityRequestAccountPtr>& new_accounts), |
| (override)); |
| |
| MOCK_METHOD(bool, |
| ShowFailureDialog, |
| (const std::string& rp_for_display, |
| const std::string& idp_for_display, |
| blink::mojom::RpContext rp_context, |
| blink::mojom::RpMode rp_mode, |
| const content::IdentityProviderMetadata& idp_metadata), |
| (override)); |
| |
| MOCK_METHOD(bool, |
| ShowErrorDialog, |
| (const std::string& rp_for_display, |
| const std::string& idp_for_display, |
| blink::mojom::RpContext rp_context, |
| blink::mojom::RpMode rp_mode, |
| const content::IdentityProviderMetadata& idp_metadata, |
| const std::optional<TokenError>& error), |
| (override)); |
| |
| MOCK_METHOD(bool, |
| ShowLoadingDialog, |
| (const std::string& rp_for_display, |
| const std::string& idp_for_display, |
| blink::mojom::RpContext rp_context, |
| blink::mojom::RpMode rp_mode), |
| (override)); |
| |
| MOCK_METHOD(std::string, GetTitle, (), (const, override)); |
| |
| MOCK_METHOD(std::optional<std::string>, GetSubtitle, (), (const, override)); |
| |
| MOCK_METHOD(void, ShowUrl, (LinkType type, const GURL& url), (override)); |
| |
| MOCK_METHOD(content::WebContents*, |
| ShowModalDialog, |
| (const GURL& url, blink::mojom::RpMode rp_mode), |
| (override)); |
| |
| MOCK_METHOD(void, CloseModalDialog, (), (override)); |
| |
| MOCK_METHOD(content::WebContents*, GetRpWebContents, (), (override)); |
| }; |
| |
| class IdentityDialogControllerTest : public ChromeRenderViewHostTestHarness { |
| public: |
| IdentityDialogControllerTest() |
| : ChromeRenderViewHostTestHarness( |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME) {} |
| ~IdentityDialogControllerTest() override = default; |
| IdentityDialogControllerTest(IdentityDialogControllerTest&) = delete; |
| IdentityDialogControllerTest& operator=(IdentityDialogControllerTest&) = |
| delete; |
| |
| void SetUp() override { |
| ChromeRenderViewHostTestHarness::SetUp(); |
| SetContents(CreateTestWebContents()); |
| NavigateAndCommit(GURL(permissions::MockPermissionRequest::kDefaultOrigin)); |
| permissions::PermissionRequestManager::CreateForWebContents(web_contents()); |
| histogram_tester_ = std::make_unique<base::HistogramTester>(); |
| } |
| |
| void TearDown() override { ChromeRenderViewHostTestHarness::TearDown(); } |
| |
| void WaitForBubbleToBeShown(permissions::PermissionRequestManager* manager) { |
| manager->DocumentOnLoadCompletedInPrimaryMainFrame(); |
| task_environment()->RunUntilIdle(); |
| } |
| |
| void Accept(permissions::PermissionRequestManager* manager) { |
| manager->Accept(); |
| task_environment()->RunUntilIdle(); |
| } |
| |
| void Deny(permissions::PermissionRequestManager* manager) { |
| manager->Deny(); |
| task_environment()->RunUntilIdle(); |
| } |
| |
| void Dismiss(permissions::PermissionRequestManager* manager) { |
| manager->Dismiss(); |
| task_environment()->RunUntilIdle(); |
| } |
| |
| std::vector<IdentityRequestAccountPtr> CreateAccount() { |
| return {base::MakeRefCounted<Account>( |
| "account_id1", "", "", "", "", "", GURL(), "", "", |
| /*login_hints=*/std::vector<std::string>(), |
| /*domain_hints=*/std::vector<std::string>(), |
| /*labels=*/std::vector<std::string>(), |
| /*login_state=*/content::IdentityRequestAccount::LoginState::kSignUp, |
| /*browser_trusted_login_state=*/ |
| content::IdentityRequestAccount::LoginState::kSignUp)}; |
| } |
| |
| IdentityProviderDataPtr CreateIdentityProviderData( |
| std::vector<IdentityRequestAccountPtr>& accounts) { |
| IdentityProviderDataPtr idp_data = |
| base::MakeRefCounted<content::IdentityProviderData>( |
| kIdpEtldPlusOne, content::IdentityProviderMetadata(), |
| content::ClientMetadata(GURL(), GURL(), GURL(), gfx::Image()), |
| blink::mojom::RpContext::kSignIn, /*format=*/std::nullopt, |
| kDefaultPermissions, |
| /*has_login_status_mismatch=*/false); |
| for (auto& account : accounts) { |
| account->identity_provider = idp_data; |
| } |
| return idp_data; |
| } |
| |
| void ShowAccountsDialog( |
| IdentityDialogController& controller, |
| blink::mojom::RpMode rp_mode, |
| DismissCallback dismiss_callback = base::DoNothing()) { |
| accounts_ = CreateAccount(); |
| IdentityProviderDataPtr idp_data = CreateIdentityProviderData(accounts_); |
| controller.ShowAccountsDialog( |
| content::RelyingPartyData(kTopFrameEtldPlusOne, |
| /*iframe_for_display=*/""), |
| {idp_data}, accounts_, |
| content::IdentityRequestAccount::SignInMode::kExplicit, rp_mode, |
| /*new_accounts=*/std::vector<IdentityRequestAccountPtr>(), |
| /*on_selected=*/base::DoNothing(), /*on_add_account=*/base::DoNothing(), |
| std::move(dismiss_callback), |
| /*accounts_displayed_callback=*/base::DoNothing()); |
| } |
| |
| segmentation_platform::MockSegmentationPlatformService* |
| CreateMockSegmentationPlatformService(const std::string& result_label, |
| base::RunLoop& run_loop) { |
| segmentation_platform_service_ = std::make_unique< |
| segmentation_platform::MockSegmentationPlatformService>(); |
| ON_CALL(*segmentation_platform_service_, |
| GetClassificationResult(_, _, _, _)) |
| .WillByDefault(testing::Invoke( |
| [&run_loop, result_label, this]( |
| auto, auto, |
| scoped_refptr<segmentation_platform::InputContext> |
| input_context, |
| segmentation_platform::ClassificationResultCallback callback) { |
| segmentation_platform::ClassificationResult result( |
| segmentation_platform::PredictionStatus::kSucceeded); |
| result.request_id = segmentation_platform::TrainingRequestId(1); |
| result.ordered_labels = {result_label}; |
| if (this->optimization_guide_decider_) { |
| ASSERT_EQ(segmentation_platform::processing::ProcessedValue( |
| web_contents()->GetLastCommittedURL().host()), |
| input_context->GetMetadataArgument( |
| segmentation_platform::kFedCmHost)); |
| ASSERT_EQ(segmentation_platform::processing::ProcessedValue( |
| web_contents()->GetLastCommittedURL()), |
| input_context->GetMetadataArgument( |
| segmentation_platform::kFedCmUrl)); |
| ASSERT_EQ(segmentation_platform::processing::ProcessedValue( |
| kPerPageLoadClickthroughRate), |
| input_context->GetMetadataArgument( |
| segmentation_platform:: |
| kFedCmPerPageLoadClickthroughRate)); |
| ASSERT_EQ(segmentation_platform::processing::ProcessedValue( |
| kPerClientClickthroughRate), |
| input_context->GetMetadataArgument( |
| segmentation_platform:: |
| kFedCmPerClientClickthroughRate)); |
| ASSERT_EQ(segmentation_platform::processing::ProcessedValue( |
| kPerImpressionClickthroughRate), |
| input_context->GetMetadataArgument( |
| segmentation_platform:: |
| kFedCmPerImpressionClickthroughRate)); |
| ASSERT_EQ(segmentation_platform::processing::ProcessedValue( |
| kLikelyToSignin), |
| input_context->GetMetadataArgument( |
| segmentation_platform::kFedCmLikelyToSignin)); |
| } |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback), result) |
| .Then(run_loop.QuitClosure())); |
| })); |
| return segmentation_platform_service_.get(); |
| } |
| |
| optimization_guide::MockOptimizationGuideDecider* |
| CreateMockOptimizationGuideDecider() { |
| optimization_guide_decider_ = |
| std::make_unique<optimization_guide::MockOptimizationGuideDecider>(); |
| EXPECT_CALL(*optimization_guide_decider_, |
| RegisterOptimizationTypes(testing::ElementsAre( |
| optimization_guide::proto::FEDCM_CLICKTHROUGH_RATE))) |
| .Times(1); |
| ON_CALL(*optimization_guide_decider_, |
| CanApplyOptimization( |
| _, |
| optimization_guide::proto::OptimizationType:: |
| FEDCM_CLICKTHROUGH_RATE, |
| testing::An<optimization_guide::OptimizationMetadata*>())) |
| .WillByDefault( |
| [](const GURL& url, |
| optimization_guide::proto::OptimizationType optimization_type, |
| optimization_guide::OptimizationMetadata* metadata) |
| -> optimization_guide::OptimizationGuideDecision { |
| *metadata = {}; |
| webid::FedCmClickthroughRateMetadata fedcm_metadata; |
| fedcm_metadata.set_per_page_load_clickthrough_rate( |
| kPerPageLoadClickthroughRate); |
| fedcm_metadata.set_per_client_clickthrough_rate( |
| kPerClientClickthroughRate); |
| fedcm_metadata.set_per_impression_clickthrough_rate( |
| kPerImpressionClickthroughRate); |
| fedcm_metadata.set_likely_to_signin(kLikelyToSignin); |
| metadata->SetAnyMetadataForTesting(fedcm_metadata); |
| return optimization_guide::OptimizationGuideDecision::kTrue; |
| }); |
| return optimization_guide_decider_.get(); |
| } |
| |
| protected: |
| std::vector<IdentityRequestAccountPtr> accounts_; |
| std::unique_ptr<base::HistogramTester> histogram_tester_; |
| std::unique_ptr<segmentation_platform::MockSegmentationPlatformService> |
| segmentation_platform_service_; |
| std::unique_ptr<optimization_guide::MockOptimizationGuideDecider> |
| optimization_guide_decider_; |
| }; |
| |
| TEST_F(IdentityDialogControllerTest, Accept) { |
| IdentityDialogController controller(web_contents()); |
| |
| base::MockCallback<base::OnceCallback<void(bool accepted)>> callback; |
| EXPECT_CALL(callback, Run(true)).WillOnce(testing::Return()); |
| controller.RequestIdPRegistrationPermision( |
| url::Origin::Create(GURL("https://idp.example")), callback.Get()); |
| |
| auto* manager = |
| permissions::PermissionRequestManager::FromWebContents(web_contents()); |
| |
| auto prompt_factory = |
| std::make_unique<permissions::MockPermissionPromptFactory>(manager); |
| |
| WaitForBubbleToBeShown(manager); |
| |
| EXPECT_TRUE(prompt_factory->is_visible()); |
| |
| Accept(manager); |
| |
| EXPECT_FALSE(prompt_factory->is_visible()); |
| } |
| |
| TEST_F(IdentityDialogControllerTest, Deny) { |
| IdentityDialogController controller(web_contents()); |
| |
| base::MockCallback<base::OnceCallback<void(bool accepted)>> callback; |
| EXPECT_CALL(callback, Run(false)).WillOnce(testing::Return()); |
| controller.RequestIdPRegistrationPermision( |
| url::Origin::Create(GURL("https://idp.example")), callback.Get()); |
| |
| auto* manager = |
| permissions::PermissionRequestManager::FromWebContents(web_contents()); |
| |
| auto prompt_factory = |
| std::make_unique<permissions::MockPermissionPromptFactory>(manager); |
| |
| WaitForBubbleToBeShown(manager); |
| |
| EXPECT_TRUE(prompt_factory->is_visible()); |
| |
| Deny(manager); |
| |
| EXPECT_FALSE(prompt_factory->is_visible()); |
| } |
| |
| TEST_F(IdentityDialogControllerTest, Dismiss) { |
| IdentityDialogController controller(web_contents()); |
| |
| base::MockCallback<base::OnceCallback<void(bool accepted)>> callback; |
| EXPECT_CALL(callback, Run(false)).WillOnce(testing::Return()); |
| controller.RequestIdPRegistrationPermision( |
| url::Origin::Create(GURL("https://idp.example")), callback.Get()); |
| |
| auto* manager = |
| permissions::PermissionRequestManager::FromWebContents(web_contents()); |
| |
| auto prompt_factory = |
| std::make_unique<permissions::MockPermissionPromptFactory>(manager); |
| |
| WaitForBubbleToBeShown(manager); |
| |
| EXPECT_TRUE(prompt_factory->is_visible()); |
| |
| Dismiss(manager); |
| |
| EXPECT_FALSE(prompt_factory->is_visible()); |
| } |
| |
| // Test that selecting an account in button mode, and then dismissing it should |
| // run the dismiss callback. |
| TEST_F(IdentityDialogControllerTest, OnAccountSelectedButtonCallsDismiss) { |
| IdentityDialogController controller(web_contents()); |
| controller.SetAccountSelectionViewForTesting( |
| std::make_unique<MockAccountSelectionView>()); |
| |
| std::vector<IdentityRequestAccountPtr> accounts = CreateAccount(); |
| IdentityProviderDataPtr idp_data = CreateIdentityProviderData(accounts); |
| |
| // Dismiss callback should run once. |
| base::MockCallback<DismissCallback> dismiss_callback; |
| EXPECT_CALL(dismiss_callback, Run).WillOnce(testing::Return()); |
| |
| ShowAccountsDialog(controller, blink::mojom::RpMode::kActive, |
| dismiss_callback.Get()); |
| |
| // User selects an account, and then dismisses it. The expectation set for |
| // dismiss callback should pass. |
| controller.OnAccountSelected(GURL(kIdpEtldPlusOne), accounts[0]->id, |
| *accounts[0]->login_state); |
| controller.OnDismiss(IdentityDialogController::DismissReason::kOther); |
| } |
| |
| // Test that selecting an account in widget, and then dismissing it should not |
| // run the dismiss callback. |
| TEST_F(IdentityDialogControllerTest, OnAccountSelectedWidgetResetsDismiss) { |
| IdentityDialogController controller(web_contents()); |
| controller.SetAccountSelectionViewForTesting( |
| std::make_unique<MockAccountSelectionView>()); |
| |
| std::vector<IdentityRequestAccountPtr> accounts = CreateAccount(); |
| IdentityProviderDataPtr idp_data = CreateIdentityProviderData(accounts); |
| |
| // Dismiss callback should not be run. |
| base::MockCallback<DismissCallback> dismiss_callback; |
| EXPECT_CALL(dismiss_callback, Run).Times(0); |
| |
| ShowAccountsDialog(controller, blink::mojom::RpMode::kPassive, |
| dismiss_callback.Get()); |
| |
| // User selects an account, and then dismisses it. The expectation set for |
| // dismiss callback should pass. |
| controller.OnAccountSelected(GURL(kIdpEtldPlusOne), accounts[0]->id, |
| *accounts[0]->login_state); |
| controller.OnDismiss(IdentityDialogController::DismissReason::kOther); |
| } |
| |
| // Crash test for crbug.com/358302105. |
| TEST_F(IdentityDialogControllerTest, NoTabDoesNotCrash) { |
| IdentityDialogController controller(web_contents()); |
| std::vector<IdentityRequestAccountPtr> accounts = CreateAccount(); |
| IdentityProviderDataPtr idp_data = CreateIdentityProviderData(accounts); |
| |
| ShowAccountsDialog(controller, blink::mojom::RpMode::kActive); |
| } |
| |
| TEST_F(IdentityDialogControllerTest, SegmentationPlatformShowUi) { |
| base::test::ScopedFeatureList list; |
| list.InitAndEnableFeature( |
| segmentation_platform::features::kSegmentationPlatformFedCmUser); |
| // Mock the segmentation platform service to return "FedCmUserLoud" as the UI |
| // volume recommendation. |
| base::RunLoop run_loop; |
| IdentityDialogController controller( |
| web_contents(), |
| CreateMockSegmentationPlatformService("FedCmUserLoud", run_loop), |
| CreateMockOptimizationGuideDecider()); |
| |
| // Show should be called. |
| std::unique_ptr<MockAccountSelectionView> account_selection_view = |
| std::make_unique<MockAccountSelectionView>(); |
| EXPECT_CALL(*account_selection_view, Show(_, _, _, _, _, _)).Times(1); |
| controller.SetAccountSelectionViewForTesting( |
| std::move(account_selection_view)); |
| |
| ShowAccountsDialog(controller, blink::mojom::RpMode::kPassive); |
| run_loop.Run(); |
| } |
| |
| TEST_F(IdentityDialogControllerTest, SegmentationPlatformDontShowUi) { |
| base::test::ScopedFeatureList list; |
| list.InitAndEnableFeature( |
| segmentation_platform::features::kSegmentationPlatformFedCmUser); |
| // Mock the segmentation platform service to return "FedCmUserQuiet" as the UI |
| // volume recommendation. |
| base::RunLoop run_loop; |
| IdentityDialogController controller( |
| web_contents(), |
| CreateMockSegmentationPlatformService("FedCmUserQuiet", run_loop), |
| CreateMockOptimizationGuideDecider()); |
| |
| // Show should not be called. |
| std::unique_ptr<MockAccountSelectionView> account_selection_view = |
| std::make_unique<MockAccountSelectionView>(); |
| EXPECT_CALL(*account_selection_view, Show(_, _, _, _, _, _)).Times(0); |
| controller.SetAccountSelectionViewForTesting( |
| std::move(account_selection_view)); |
| |
| // Dismiss callback should be run. |
| base::MockCallback<DismissCallback> dismiss_callback; |
| EXPECT_CALL(dismiss_callback, Run).WillOnce(testing::Return()); |
| |
| ShowAccountsDialog(controller, blink::mojom::RpMode::kPassive, |
| dismiss_callback.Get()); |
| run_loop.Run(); |
| } |
| |
| TEST_F(IdentityDialogControllerTest, |
| SegmentationPlatformTrainingDataCollection) { |
| base::test::ScopedFeatureList list; |
| list.InitAndEnableFeature( |
| segmentation_platform::features::kSegmentationPlatformFedCmUser); |
| |
| auto CheckForSampleAndReset([&](IdentityDialogController::UserAction action) { |
| histogram_tester_->ExpectUniqueSample( |
| "Blink.FedCm.SegmentationPlatform.UserAction", static_cast<int>(action), |
| 1); |
| histogram_tester_ = std::make_unique<base::HistogramTester>(); |
| }); |
| |
| { |
| // User proceeds with an account. |
| base::RunLoop run_loop; |
| IdentityDialogController controller( |
| web_contents(), |
| CreateMockSegmentationPlatformService("FedCmUserLoud", run_loop), |
| CreateMockOptimizationGuideDecider()); |
| controller.SetAccountSelectionViewForTesting( |
| std::make_unique<MockAccountSelectionView>()); |
| EXPECT_CALL(*segmentation_platform_service_, |
| CollectTrainingData(_, _, _, _, _)) |
| .Times(1); |
| |
| ShowAccountsDialog(controller, blink::mojom::RpMode::kPassive); |
| run_loop.Run(); |
| |
| // User selects an account. |
| controller.OnAccountSelected(GURL(kIdpEtldPlusOne), accounts_[0]->id, |
| *accounts_[0]->login_state); |
| } |
| CheckForSampleAndReset(IdentityDialogController::UserAction::kSuccess); |
| |
| { |
| // User clicks on close button. |
| base::RunLoop run_loop; |
| IdentityDialogController controller( |
| web_contents(), |
| CreateMockSegmentationPlatformService("FedCmUserLoud", run_loop), |
| CreateMockOptimizationGuideDecider()); |
| controller.SetAccountSelectionViewForTesting( |
| std::make_unique<MockAccountSelectionView>()); |
| EXPECT_CALL(*segmentation_platform_service_, |
| CollectTrainingData(_, _, _, _, _)) |
| .Times(1); |
| |
| ShowAccountsDialog(controller, blink::mojom::RpMode::kPassive); |
| run_loop.Run(); |
| |
| // User closes the dialog. |
| controller.OnDismiss(IdentityDialogController::DismissReason::kCloseButton); |
| } |
| CheckForSampleAndReset(IdentityDialogController::UserAction::kClosed); |
| |
| { |
| // User ignores the UI. |
| base::RunLoop run_loop; |
| IdentityDialogController controller( |
| web_contents(), |
| CreateMockSegmentationPlatformService("FedCmUserLoud", run_loop), |
| CreateMockOptimizationGuideDecider()); |
| controller.SetAccountSelectionViewForTesting( |
| std::make_unique<MockAccountSelectionView>()); |
| EXPECT_CALL(*segmentation_platform_service_, |
| CollectTrainingData(_, _, _, _, _)) |
| .Times(1); |
| |
| ShowAccountsDialog(controller, blink::mojom::RpMode::kPassive); |
| run_loop.Run(); |
| |
| // Dialog gets dismissed for other reasons. |
| controller.OnDismiss(IdentityDialogController::DismissReason::kOther); |
| } |
| CheckForSampleAndReset(IdentityDialogController::UserAction::kIgnored); |
| } |