| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/signin/dice_web_signin_interceptor.h" |
| |
| #include <memory> |
| |
| #include "base/callback.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/profiles/profile_attributes_entry.h" |
| #include "chrome/browser/profiles/profile_attributes_storage.h" |
| #include "chrome/browser/signin/chrome_signin_client_factory.h" |
| #include "chrome/browser/signin/chrome_signin_client_test_util.h" |
| #include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" |
| #include "chrome/browser/signin/signin_features.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/common/chrome_constants.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/test/base/browser_with_test_window_test.h" |
| #include "chrome/test/base/testing_browser_process.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "chrome/test/base/testing_profile_manager.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/signin/public/identity_manager/account_info.h" |
| #include "components/signin/public/identity_manager/identity_test_environment.h" |
| #include "services/network/test/test_url_loader_factory.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| |
| class MockDiceWebSigninInterceptorDelegate |
| : public DiceWebSigninInterceptor::Delegate { |
| public: |
| MOCK_METHOD(std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>, |
| ShowSigninInterceptionBubble, |
| (content::WebContents * web_contents, |
| const BubbleParameters& bubble_parameters, |
| base::OnceCallback<void(SigninInterceptionResult)> callback), |
| (override)); |
| void ShowProfileCustomizationBubble(Browser* browser) override {} |
| }; |
| |
| // Matches BubbleParameters fields excepting the color. This is useful in the |
| // test because the color is randomly generated. |
| testing::Matcher<const DiceWebSigninInterceptor::Delegate::BubbleParameters&> |
| MatchBubbleParameters( |
| const DiceWebSigninInterceptor::Delegate::BubbleParameters& parameters) { |
| return testing::AllOf( |
| testing::Field("interception_type", |
| &DiceWebSigninInterceptor::Delegate::BubbleParameters:: |
| interception_type, |
| parameters.interception_type), |
| testing::Field("intercepted_account", |
| &DiceWebSigninInterceptor::Delegate::BubbleParameters:: |
| intercepted_account, |
| parameters.intercepted_account), |
| testing::Field("primary_account", |
| &DiceWebSigninInterceptor::Delegate::BubbleParameters:: |
| primary_account, |
| parameters.primary_account)); |
| } |
| |
| // If the account info is valid, does nothing. Otherwise fills the extended |
| // fields with default values. |
| void MakeValidAccountInfo(AccountInfo* info) { |
| if (info->IsValid()) |
| return; |
| info->full_name = "fullname"; |
| info->given_name = "givenname"; |
| info->hosted_domain = kNoHostedDomainFound; |
| info->locale = "en"; |
| info->picture_url = "https://example.com"; |
| info->is_child_account = false; |
| DCHECK(info->IsValid()); |
| } |
| |
| } // namespace |
| |
| class DiceWebSigninInterceptorTest : public BrowserWithTestWindowTest { |
| public: |
| DiceWebSigninInterceptorTest() = default; |
| ~DiceWebSigninInterceptorTest() override = default; |
| |
| DiceWebSigninInterceptor* interceptor() { |
| return dice_web_signin_interceptor_.get(); |
| } |
| |
| MockDiceWebSigninInterceptorDelegate* mock_delegate() { |
| return mock_delegate_; |
| } |
| |
| content::WebContents* web_contents() { |
| return browser()->tab_strip_model()->GetActiveWebContents(); |
| } |
| |
| ProfileAttributesStorage* profile_attributes_storage() { |
| return profile_manager()->profile_attributes_storage(); |
| } |
| |
| signin::IdentityTestEnvironment* identity_test_env() { |
| return identity_test_env_profile_adaptor_->identity_test_env(); |
| } |
| |
| Profile* CreateTestingProfile(const std::string& name) { |
| return profile_manager()->CreateTestingProfile(name); |
| } |
| |
| // Helper function that calls MaybeInterceptWebSignin with parameters |
| // compatible with interception. |
| void MaybeIntercept(CoreAccountId account_id) { |
| interceptor()->MaybeInterceptWebSignin(web_contents(), account_id, |
| /*is_new_account=*/true, |
| /*is_sync_signin=*/false); |
| } |
| |
| // Calls MaybeInterceptWebSignin and verifies the heuristic outcome, the |
| // histograms and whether the interception is in progress. |
| // This function only works if the interception decision can be made |
| // synchronously (GetHeuristicOutcome() returns a value). |
| void TestSynchronousInterception( |
| AccountInfo account_info, |
| bool is_new_account, |
| bool is_sync_signin, |
| SigninInterceptionHeuristicOutcome expected_outcome) { |
| ASSERT_EQ(interceptor()->GetHeuristicOutcome(is_new_account, is_sync_signin, |
| account_info.email, |
| /*entry=*/nullptr), |
| expected_outcome); |
| base::HistogramTester histogram_tester; |
| interceptor()->MaybeInterceptWebSignin(web_contents(), |
| account_info.account_id, |
| is_new_account, is_sync_signin); |
| testing::Mock::VerifyAndClearExpectations(mock_delegate()); |
| histogram_tester.ExpectUniqueSample("Signin.Intercept.HeuristicOutcome", |
| expected_outcome, 1); |
| EXPECT_EQ(interceptor()->is_interception_in_progress(), |
| SigninInterceptionHeuristicOutcomeIsSuccess(expected_outcome)); |
| } |
| |
| private: |
| // testing::Test: |
| void SetUp() override { |
| feature_list_.InitAndEnableFeature(kDiceWebSigninInterceptionFeature); |
| BrowserWithTestWindowTest::SetUp(); |
| |
| identity_test_env_profile_adaptor_ = |
| std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile()); |
| identity_test_env_profile_adaptor_->identity_test_env() |
| ->SetTestURLLoaderFactory(&test_url_loader_factory_); |
| |
| auto delegate = std::make_unique< |
| testing::StrictMock<MockDiceWebSigninInterceptorDelegate>>(); |
| mock_delegate_ = delegate.get(); |
| dice_web_signin_interceptor_ = std::make_unique<DiceWebSigninInterceptor>( |
| profile(), std::move(delegate)); |
| |
| // Create the first tab so that web_contents() exists. |
| AddTab(browser(), GURL("http://foo/1")); |
| } |
| |
| void TearDown() override { |
| dice_web_signin_interceptor_->Shutdown(); |
| identity_test_env_profile_adaptor_.reset(); |
| BrowserWithTestWindowTest::TearDown(); |
| } |
| |
| TestingProfile::TestingFactories GetTestingFactories() override { |
| TestingProfile::TestingFactories factories = |
| IdentityTestEnvironmentProfileAdaptor:: |
| GetIdentityTestEnvironmentFactories(); |
| factories.push_back( |
| {ChromeSigninClientFactory::GetInstance(), |
| base::BindRepeating(&BuildChromeSigninClientWithURLLoader, |
| &test_url_loader_factory_)}); |
| return factories; |
| } |
| |
| base::test::ScopedFeatureList feature_list_; |
| network::TestURLLoaderFactory test_url_loader_factory_; |
| std::unique_ptr<IdentityTestEnvironmentProfileAdaptor> |
| identity_test_env_profile_adaptor_; |
| std::unique_ptr<DiceWebSigninInterceptor> dice_web_signin_interceptor_; |
| MockDiceWebSigninInterceptorDelegate* mock_delegate_ = nullptr; |
| }; |
| |
| TEST_F(DiceWebSigninInterceptorTest, ShouldShowProfileSwitchBubble) { |
| AccountInfo account_info = |
| identity_test_env()->MakeAccountAvailable("bob@example.com"); |
| const std::string& email = account_info.email; |
| EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble( |
| email, profile_attributes_storage())); |
| |
| // Add another profile with no account. |
| CreateTestingProfile("Profile 1"); |
| EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble( |
| email, profile_attributes_storage())); |
| |
| // Add another profile with a different account. |
| Profile* profile_2 = CreateTestingProfile("Profile 2"); |
| ProfileAttributesEntry* entry = nullptr; |
| ASSERT_TRUE(profile_attributes_storage()->GetProfileAttributesWithPath( |
| profile_2->GetPath(), &entry)); |
| std::string kOtherGaiaID = "SomeOtherGaiaID"; |
| ASSERT_NE(kOtherGaiaID, account_info.gaia); |
| entry->SetAuthInfo(kOtherGaiaID, base::UTF8ToUTF16("alice@gmail.com"), |
| /*is_consented_primary_account=*/true); |
| EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble( |
| email, profile_attributes_storage())); |
| |
| // Change the account to match. |
| entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), |
| /*is_consented_primary_account=*/false); |
| const ProfileAttributesEntry* switch_to_entry = |
| interceptor()->ShouldShowProfileSwitchBubble( |
| email, profile_attributes_storage()); |
| EXPECT_EQ(entry, switch_to_entry); |
| } |
| |
| TEST_F(DiceWebSigninInterceptorTest, NoBubbleWithSingleAccount) { |
| AccountInfo account_info = |
| identity_test_env()->MakeAccountAvailable("bob@example.com"); |
| MakeValidAccountInfo(&account_info); |
| account_info.hosted_domain = "example.com"; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info); |
| |
| // Without UPA. |
| EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info)); |
| EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info)); |
| |
| // With UPA. |
| identity_test_env()->SetUnconsentedPrimaryAccount("bob@example.com"); |
| EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info)); |
| } |
| |
| TEST_F(DiceWebSigninInterceptorTest, ShouldShowEnterpriseBubble) { |
| // Setup 3 accounts in the profile: |
| // - primary account |
| // - other enterprise account that is not primary (should be ignored) |
| // - intercepted account. |
| AccountInfo primary_account_info = |
| identity_test_env()->MakeUnconsentedPrimaryAccountAvailable( |
| "alice@example.com"); |
| AccountInfo other_account_info = |
| identity_test_env()->MakeAccountAvailable("dummy@example.com"); |
| MakeValidAccountInfo(&other_account_info); |
| other_account_info.hosted_domain = "example.com"; |
| identity_test_env()->UpdateAccountInfoForAccount(other_account_info); |
| AccountInfo account_info = |
| identity_test_env()->MakeAccountAvailable("bob@example.com"); |
| MakeValidAccountInfo(&account_info); |
| identity_test_env()->UpdateAccountInfoForAccount(account_info); |
| ASSERT_EQ(identity_test_env()->identity_manager()->GetPrimaryAccountId( |
| signin::ConsentLevel::kNotRequired), |
| primary_account_info.account_id); |
| |
| // The primary account does not have full account info (empty domain). |
| ASSERT_TRUE(identity_test_env() |
| ->identity_manager() |
| ->FindExtendedAccountInfoForAccountWithRefreshToken( |
| primary_account_info) |
| ->hosted_domain.empty()); |
| EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info)); |
| account_info.hosted_domain = "example.com"; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info); |
| EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info)); |
| |
| // The primary account has full info. |
| MakeValidAccountInfo(&primary_account_info); |
| identity_test_env()->UpdateAccountInfoForAccount(primary_account_info); |
| // The intercepted account is enterprise. |
| EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info)); |
| // Two consummer accounts. |
| account_info.hosted_domain = kNoHostedDomainFound; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info); |
| EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info)); |
| // The primary account is enterprise. |
| primary_account_info.hosted_domain = "example.com"; |
| identity_test_env()->UpdateAccountInfoForAccount(primary_account_info); |
| EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info)); |
| } |
| |
| TEST_F(DiceWebSigninInterceptorTest, ShouldShowEnterpriseBubbleWithoutUPA) { |
| AccountInfo account_info_1 = |
| identity_test_env()->MakeAccountAvailable("bob@example.com"); |
| MakeValidAccountInfo(&account_info_1); |
| account_info_1.hosted_domain = "example.com"; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info_1); |
| AccountInfo account_info_2 = |
| identity_test_env()->MakeAccountAvailable("alice@example.com"); |
| MakeValidAccountInfo(&account_info_2); |
| account_info_2.hosted_domain = "example.com"; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info_2); |
| |
| // Primary account is not set. |
| ASSERT_FALSE(identity_test_env()->identity_manager()->HasPrimaryAccount( |
| signin::ConsentLevel::kNotRequired)); |
| EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info_1)); |
| } |
| |
| TEST_F(DiceWebSigninInterceptorTest, ShouldShowMultiUserBubble) { |
| // Setup two accounts in the profile. |
| AccountInfo account_info_1 = |
| identity_test_env()->MakeAccountAvailable("bob@example.com"); |
| MakeValidAccountInfo(&account_info_1); |
| account_info_1.given_name = "Bob"; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info_1); |
| AccountInfo account_info_2 = |
| identity_test_env()->MakeAccountAvailable("alice@example.com"); |
| |
| // The other account does not have full account info (empty name). |
| ASSERT_TRUE(account_info_2.given_name.empty()); |
| EXPECT_TRUE(interceptor()->ShouldShowMultiUserBubble(account_info_1)); |
| |
| // Accounts with different names. |
| account_info_1.given_name = "Bob"; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info_1); |
| MakeValidAccountInfo(&account_info_2); |
| account_info_2.given_name = "Alice"; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info_2); |
| EXPECT_TRUE(interceptor()->ShouldShowMultiUserBubble(account_info_1)); |
| |
| // Accounts with same names. |
| account_info_1.given_name = "Alice"; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info_1); |
| EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info_1)); |
| |
| // Comparison is case insensitive. |
| account_info_1.given_name = "alice"; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info_1); |
| EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info_1)); |
| } |
| |
| TEST_F(DiceWebSigninInterceptorTest, NoInterception) { |
| // Setup for profile switch interception. |
| std::string email = "bob@example.com"; |
| AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email); |
| Profile* profile_2 = CreateTestingProfile("Profile 2"); |
| ProfileAttributesEntry* entry = nullptr; |
| ASSERT_TRUE(profile_attributes_storage()->GetProfileAttributesWithPath( |
| profile_2->GetPath(), &entry)); |
| entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), |
| /*is_consented_primary_account=*/false); |
| |
| // Check that Sync signin is not intercepted. |
| TestSynchronousInterception( |
| account_info, /*is_new_account=*/true, /*is_sync_signin=*/true, |
| SigninInterceptionHeuristicOutcome::kAbortSyncSignin); |
| |
| // Check that reauth is not intercepted. |
| TestSynchronousInterception( |
| account_info, /*is_new_account=*/false, /*is_sync_signin=*/false, |
| SigninInterceptionHeuristicOutcome::kAbortAccountNotNew); |
| |
| // Check that interception works otherwise, as a sanity check. |
| DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { |
| DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch, |
| account_info, AccountInfo(), SkColor()}; |
| EXPECT_CALL(*mock_delegate(), |
| ShowSigninInterceptionBubble( |
| web_contents(), MatchBubbleParameters(expected_parameters), |
| testing::_)); |
| TestSynchronousInterception( |
| account_info, /*is_new_account=*/true, /*is_sync_signin=*/false, |
| SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); |
| } |
| |
| // Checks that the heuristic still works if the account was not added to Chrome |
| // yet. |
| TEST_F(DiceWebSigninInterceptorTest, HeuristicAccountNotAdded) { |
| // Setup for profile switch interception. |
| std::string email = "bob@example.com"; |
| Profile* profile_2 = CreateTestingProfile("Profile 2"); |
| ProfileAttributesEntry* entry = nullptr; |
| ASSERT_TRUE(profile_attributes_storage()->GetProfileAttributesWithPath( |
| profile_2->GetPath(), &entry)); |
| entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email), |
| /*is_consented_primary_account=*/false); |
| EXPECT_EQ(interceptor()->GetHeuristicOutcome( |
| /*is_new_account=*/true, /*is_sync_signin=*/false, email, |
| /*entry=*/nullptr), |
| SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); |
| } |
| |
| // Checks that the heuristic defaults to gmail.com when no domain is specified. |
| TEST_F(DiceWebSigninInterceptorTest, HeuristicDefaultsToGmail) { |
| // Setup for profile switch interception. |
| std::string email = "bob@gmail.com"; |
| Profile* profile_2 = CreateTestingProfile("Profile 2"); |
| ProfileAttributesEntry* entry = nullptr; |
| ASSERT_TRUE(profile_attributes_storage()->GetProfileAttributesWithPath( |
| profile_2->GetPath(), &entry)); |
| entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email), |
| /*is_consented_primary_account=*/false); |
| // No domain defaults to gmail.com |
| EXPECT_EQ(interceptor()->GetHeuristicOutcome( |
| /*is_new_account=*/true, /*is_sync_signin=*/false, "bob", |
| /*entry=*/nullptr), |
| SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); |
| // Using wrong domain does not trigger the interception. |
| EXPECT_EQ( |
| interceptor()->GetHeuristicOutcome( |
| /*is_new_account=*/true, /*is_sync_signin=*/false, "bob@example.com", |
| /*entry=*/nullptr), |
| SigninInterceptionHeuristicOutcome::kAbortSingleAccount); |
| } |
| |
| // Checks that no heuristic is returned if signin interception is disabled. |
| TEST_F(DiceWebSigninInterceptorTest, InterceptionDsiabled) { |
| // Setup for profile switch interception. |
| std::string email = "bob@gmail.com"; |
| Profile* profile_2 = CreateTestingProfile("Profile 2"); |
| profile()->GetPrefs()->SetBoolean(prefs::kSigninInterceptionEnabled, false); |
| ProfileAttributesEntry* entry = nullptr; |
| ASSERT_TRUE(profile_attributes_storage()->GetProfileAttributesWithPath( |
| profile_2->GetPath(), &entry)); |
| entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email), |
| /*is_consented_primary_account=*/false); |
| EXPECT_EQ(interceptor()->GetHeuristicOutcome( |
| /*is_new_account=*/true, /*is_sync_signin=*/false, "bob", |
| /*entry=*/nullptr), |
| SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled); |
| EXPECT_EQ( |
| interceptor()->GetHeuristicOutcome( |
| /*is_new_account=*/true, /*is_sync_signin=*/false, "bob@example.com", |
| /*entry=*/nullptr), |
| SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled); |
| } |
| |
| TEST_F(DiceWebSigninInterceptorTest, InterceptionInProgress) { |
| // Setup for profile switch interception. |
| std::string email = "bob@example.com"; |
| AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email); |
| Profile* profile_2 = CreateTestingProfile("Profile 2"); |
| ProfileAttributesEntry* entry = nullptr; |
| ASSERT_TRUE(profile_attributes_storage()->GetProfileAttributesWithPath( |
| profile_2->GetPath(), &entry)); |
| entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), |
| /*is_consented_primary_account=*/false); |
| |
| // Start an interception. |
| DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { |
| DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch, |
| account_info, AccountInfo(), SkColor()}; |
| base::OnceCallback<void(SigninInterceptionResult)> delegate_callback; |
| EXPECT_CALL(*mock_delegate(), |
| ShowSigninInterceptionBubble( |
| web_contents(), MatchBubbleParameters(expected_parameters), |
| testing::_)) |
| .WillOnce(testing::WithArg<2>(testing::Invoke( |
| [&delegate_callback]( |
| base::OnceCallback<void(SigninInterceptionResult)> callback) { |
| delegate_callback = std::move(callback); |
| return nullptr; |
| }))); |
| MaybeIntercept(account_info.account_id); |
| testing::Mock::VerifyAndClearExpectations(mock_delegate()); |
| EXPECT_TRUE(interceptor()->is_interception_in_progress()); |
| |
| // Check that there is no interception while another one is in progress. |
| base::HistogramTester histogram_tester; |
| MaybeIntercept(account_info.account_id); |
| testing::Mock::VerifyAndClearExpectations(mock_delegate()); |
| histogram_tester.ExpectUniqueSample( |
| "Signin.Intercept.HeuristicOutcome", |
| SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress, 1); |
| |
| // Complete the interception that was in progress. |
| std::move(delegate_callback).Run(SigninInterceptionResult::kDeclined); |
| EXPECT_FALSE(interceptor()->is_interception_in_progress()); |
| |
| // A new interception can now start. |
| EXPECT_CALL(*mock_delegate(), |
| ShowSigninInterceptionBubble( |
| web_contents(), MatchBubbleParameters(expected_parameters), |
| testing::_)); |
| MaybeIntercept(account_info.account_id); |
| } |
| |
| TEST_F(DiceWebSigninInterceptorTest, DeclineCreationRepeatedly) { |
| base::HistogramTester histogram_tester; |
| AccountInfo primary_account_info = |
| identity_test_env()->MakeUnconsentedPrimaryAccountAvailable( |
| "bob@example.com"); |
| AccountInfo account_info = |
| identity_test_env()->MakeAccountAvailable("alice@example.com"); |
| MakeValidAccountInfo(&account_info); |
| account_info.hosted_domain = "example.com"; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info); |
| |
| const int kMaxProfileCreationDeclinedCount = 2; |
| // Decline the interception kMaxProfileCreationDeclinedCount times. |
| DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { |
| DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise, |
| account_info, primary_account_info, SkColor()}; |
| for (int i = 0; i < kMaxProfileCreationDeclinedCount; ++i) { |
| EXPECT_CALL(*mock_delegate(), |
| ShowSigninInterceptionBubble( |
| web_contents(), MatchBubbleParameters(expected_parameters), |
| testing::_)) |
| .WillOnce(testing::WithArg<2>(testing::Invoke( |
| [](base::OnceCallback<void(SigninInterceptionResult)> callback) { |
| std::move(callback).Run(SigninInterceptionResult::kDeclined); |
| return nullptr; |
| }))); |
| MaybeIntercept(account_info.account_id); |
| EXPECT_EQ(interceptor()->is_interception_in_progress(), false); |
| histogram_tester.ExpectUniqueSample( |
| "Signin.Intercept.HeuristicOutcome", |
| SigninInterceptionHeuristicOutcome::kInterceptEnterprise, i + 1); |
| } |
| |
| // Next time the interception is not shown again. |
| MaybeIntercept(account_info.account_id); |
| EXPECT_EQ(interceptor()->is_interception_in_progress(), false); |
| histogram_tester.ExpectBucketCount( |
| "Signin.Intercept.HeuristicOutcome", |
| SigninInterceptionHeuristicOutcome::kAbortUserDeclinedProfileForAccount, |
| 1); |
| |
| // Another account can still be intercepted. |
| account_info.email = "oscar@example.com"; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info); |
| expected_parameters = { |
| DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise, |
| account_info, primary_account_info, SkColor()}; |
| EXPECT_CALL(*mock_delegate(), |
| ShowSigninInterceptionBubble( |
| web_contents(), MatchBubbleParameters(expected_parameters), |
| testing::_)); |
| MaybeIntercept(account_info.account_id); |
| histogram_tester.ExpectBucketCount( |
| "Signin.Intercept.HeuristicOutcome", |
| SigninInterceptionHeuristicOutcome::kInterceptEnterprise, |
| kMaxProfileCreationDeclinedCount + 1); |
| EXPECT_EQ(interceptor()->is_interception_in_progress(), true); |
| } |
| |
| TEST_F(DiceWebSigninInterceptorTest, DeclineSwitchRepeatedly_NoLimit) { |
| base::HistogramTester histogram_tester; |
| // Setup for profile switch interception. |
| std::string email = "bob@example.com"; |
| AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email); |
| Profile* profile_2 = CreateTestingProfile("Profile 2"); |
| ProfileAttributesEntry* entry = nullptr; |
| ASSERT_TRUE(profile_attributes_storage()->GetProfileAttributesWithPath( |
| profile_2->GetPath(), &entry)); |
| entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), |
| /*is_consented_primary_account=*/false); |
| |
| // Test that the profile switch can be declined multiple times. |
| DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { |
| DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch, |
| account_info, AccountInfo(), SkColor()}; |
| for (int i = 0; i < 10; ++i) { |
| EXPECT_CALL(*mock_delegate(), |
| ShowSigninInterceptionBubble( |
| web_contents(), MatchBubbleParameters(expected_parameters), |
| testing::_)) |
| .WillOnce(testing::WithArg<2>(testing::Invoke( |
| [](base::OnceCallback<void(SigninInterceptionResult)> callback) { |
| std::move(callback).Run(SigninInterceptionResult::kDeclined); |
| return nullptr; |
| }))); |
| MaybeIntercept(account_info.account_id); |
| EXPECT_EQ(interceptor()->is_interception_in_progress(), false); |
| histogram_tester.ExpectUniqueSample( |
| "Signin.Intercept.HeuristicOutcome", |
| SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch, i + 1); |
| } |
| } |
| |
| TEST_F(DiceWebSigninInterceptorTest, |
| DeclineSwitchRepeatedly_LimitedByExperiment) { |
| const int kMaxProfileSwitchDeclinedCount = 3; |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| kDiceWebSigninInterceptionFeature, |
| {{"max_profile_switch_declined_count", |
| base::NumberToString(kMaxProfileSwitchDeclinedCount)}}); |
| |
| base::HistogramTester histogram_tester; |
| // Setup for profile switch interception. |
| std::string email = "bob@example.com"; |
| AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email); |
| Profile* profile_2 = CreateTestingProfile("Profile 2"); |
| ProfileAttributesEntry* entry = nullptr; |
| ASSERT_TRUE(profile_attributes_storage()->GetProfileAttributesWithPath( |
| profile_2->GetPath(), &entry)); |
| entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), |
| /*is_consented_primary_account=*/false); |
| |
| // Decline the interception kMaxProfileSwitchDeclinedCount times. |
| DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { |
| DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch, |
| account_info, AccountInfo(), SkColor()}; |
| for (int i = 0; i < kMaxProfileSwitchDeclinedCount; ++i) { |
| EXPECT_CALL(*mock_delegate(), |
| ShowSigninInterceptionBubble( |
| web_contents(), MatchBubbleParameters(expected_parameters), |
| testing::_)) |
| .WillOnce(testing::WithArg<2>(testing::Invoke( |
| [](base::OnceCallback<void(SigninInterceptionResult)> callback) { |
| std::move(callback).Run(SigninInterceptionResult::kDeclined); |
| return nullptr; |
| }))); |
| MaybeIntercept(account_info.account_id); |
| EXPECT_EQ(interceptor()->is_interception_in_progress(), false); |
| histogram_tester.ExpectUniqueSample( |
| "Signin.Intercept.HeuristicOutcome", |
| SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch, i + 1); |
| } |
| |
| // Next time the interception is not shown again. |
| MaybeIntercept(account_info.account_id); |
| EXPECT_EQ(interceptor()->is_interception_in_progress(), false); |
| histogram_tester.ExpectBucketCount( |
| "Signin.Intercept.HeuristicOutcome", |
| SigninInterceptionHeuristicOutcome::kAbortUserDeclinedProfileForAccount, |
| 1); |
| |
| // Another account can still be intercepted. |
| account_info.email = "oscar@example.com"; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info); |
| Profile* profile_3 = CreateTestingProfile("Profile 3"); |
| ProfileAttributesEntry* entry_2 = nullptr; |
| ASSERT_TRUE(profile_attributes_storage()->GetProfileAttributesWithPath( |
| profile_3->GetPath(), &entry_2)); |
| entry_2->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(account_info.email), |
| /*is_consented_primary_account=*/false); |
| |
| expected_parameters = { |
| DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch, |
| account_info, AccountInfo(), SkColor()}; |
| EXPECT_CALL(*mock_delegate(), |
| ShowSigninInterceptionBubble( |
| web_contents(), MatchBubbleParameters(expected_parameters), |
| testing::_)); |
| MaybeIntercept(account_info.account_id); |
| histogram_tester.ExpectBucketCount( |
| "Signin.Intercept.HeuristicOutcome", |
| SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch, |
| kMaxProfileSwitchDeclinedCount + 1); |
| EXPECT_EQ(interceptor()->is_interception_in_progress(), true); |
| } |
| |
| TEST_F(DiceWebSigninInterceptorTest, |
| DeclineSwitchRepeatedly_DisabledByExperiment) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| kDiceWebSigninInterceptionFeature, |
| {{"max_profile_switch_declined_count", "0"}}); |
| |
| base::HistogramTester histogram_tester; |
| // Setup for profile switch interception. |
| std::string email = "bob@example.com"; |
| AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email); |
| Profile* profile_2 = CreateTestingProfile("Profile 2"); |
| ProfileAttributesEntry* entry = nullptr; |
| ASSERT_TRUE(profile_attributes_storage()->GetProfileAttributesWithPath( |
| profile_2->GetPath(), &entry)); |
| entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), |
| /*is_consented_primary_account=*/false); |
| |
| // The interception is not shown even at first attempt. |
| MaybeIntercept(account_info.account_id); |
| EXPECT_EQ(interceptor()->is_interception_in_progress(), false); |
| histogram_tester.ExpectBucketCount( |
| "Signin.Intercept.HeuristicOutcome", |
| SigninInterceptionHeuristicOutcome::kAbortUserDeclinedProfileForAccount, |
| 1); |
| } |
| |
| TEST_F(DiceWebSigninInterceptorTest, PersistentHash) { |
| // The hash is persistent (the value should never change). |
| EXPECT_EQ("email_174", |
| interceptor()->GetPersistentEmailHash("alice@example.com")); |
| // Different email get another hash. |
| EXPECT_NE(interceptor()->GetPersistentEmailHash("bob@gmail.com"), |
| interceptor()->GetPersistentEmailHash("alice@example.com")); |
| // Equivalent emails get the same hash. |
| EXPECT_EQ(interceptor()->GetPersistentEmailHash("bob"), |
| interceptor()->GetPersistentEmailHash("bob@gmail.com")); |
| EXPECT_EQ(interceptor()->GetPersistentEmailHash("bo.b@gmail.com"), |
| interceptor()->GetPersistentEmailHash("bob@gmail.com")); |
| // Dots are removed only for gmail accounts. |
| EXPECT_NE(interceptor()->GetPersistentEmailHash("alice@example.com"), |
| interceptor()->GetPersistentEmailHash("al.ice@example.com")); |
| } |
| |
| // Interception other than the profile switch require at least 2 accounts. |
| TEST_F(DiceWebSigninInterceptorTest, NoInterceptionWithOneAccount) { |
| base::HistogramTester histogram_tester; |
| AccountInfo account_info = |
| identity_test_env()->MakeAccountAvailable("bob@example.com"); |
| // Interception aborts even if the account info is not available. |
| ASSERT_FALSE( |
| identity_test_env() |
| ->identity_manager() |
| ->FindExtendedAccountInfoForAccountWithRefreshTokenByAccountId( |
| account_info.account_id) |
| ->IsValid()); |
| TestSynchronousInterception( |
| account_info, /*is_new_account=*/true, /*is_sync_signin=*/false, |
| SigninInterceptionHeuristicOutcome::kAbortSingleAccount); |
| } |
| |
| // When profile creation is disallowed, profile switch interception is still |
| // enabled, but others are disabled. |
| TEST_F(DiceWebSigninInterceptorTest, ProfileCreationDisallowed) { |
| base::HistogramTester histogram_tester; |
| g_browser_process->local_state()->SetBoolean(prefs::kBrowserAddPersonEnabled, |
| false); |
| // Setup for profile switch interception. |
| std::string email = "bob@example.com"; |
| AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email); |
| AccountInfo other_account_info = |
| identity_test_env()->MakeAccountAvailable("alice@example.com"); |
| Profile* profile_2 = CreateTestingProfile("Profile 2"); |
| ProfileAttributesEntry* entry = nullptr; |
| ASSERT_TRUE(profile_attributes_storage()->GetProfileAttributesWithPath( |
| profile_2->GetPath(), &entry)); |
| entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), |
| /*is_consented_primary_account=*/false); |
| |
| // Interception that would offer creating a new profile does not work. |
| TestSynchronousInterception( |
| other_account_info, /*is_new_account=*/true, /*is_sync_signin=*/false, |
| SigninInterceptionHeuristicOutcome::kAbortProfileCreationDisallowed); |
| |
| // Profile switch interception still works. |
| DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { |
| DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch, |
| account_info, AccountInfo(), SkColor()}; |
| EXPECT_CALL(*mock_delegate(), |
| ShowSigninInterceptionBubble( |
| web_contents(), MatchBubbleParameters(expected_parameters), |
| testing::_)); |
| MaybeIntercept(account_info.account_id); |
| } |
| |
| TEST_F(DiceWebSigninInterceptorTest, WaitForAccountInfoAvailable) { |
| base::HistogramTester histogram_tester; |
| AccountInfo primary_account_info = |
| identity_test_env()->MakeUnconsentedPrimaryAccountAvailable( |
| "bob@example.com"); |
| AccountInfo account_info = |
| identity_test_env()->MakeAccountAvailable("alice@example.com"); |
| EXPECT_FALSE(interceptor() |
| ->GetHeuristicOutcome(/*is_new_account=*/true, |
| /*is_sync_signin=*/false, |
| account_info.email, |
| /*entry=*/nullptr) |
| .has_value()); |
| MaybeIntercept(account_info.account_id); |
| // Delegate was not called yet. |
| testing::Mock::VerifyAndClearExpectations(mock_delegate()); |
| |
| // Account info becomes available, interception happens. |
| DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { |
| DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise, |
| account_info, primary_account_info, SkColor()}; |
| EXPECT_CALL(*mock_delegate(), |
| ShowSigninInterceptionBubble( |
| web_contents(), MatchBubbleParameters(expected_parameters), |
| testing::_)); |
| MakeValidAccountInfo(&account_info); |
| account_info.hosted_domain = "example.com"; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info); |
| histogram_tester.ExpectTotalCount("Signin.Intercept.AccountInfoFetchDuration", |
| 1); |
| } |
| |
| TEST_F(DiceWebSigninInterceptorTest, AccountInfoAlreadyAvailable) { |
| base::HistogramTester histogram_tester; |
| AccountInfo primary_account_info = |
| identity_test_env()->MakeUnconsentedPrimaryAccountAvailable( |
| "bob@example.com"); |
| AccountInfo account_info = |
| identity_test_env()->MakeAccountAvailable("alice@example.com"); |
| MakeValidAccountInfo(&account_info); |
| account_info.hosted_domain = "example.com"; |
| identity_test_env()->UpdateAccountInfoForAccount(account_info); |
| |
| // Account info is already available, interception happens immediately. |
| DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { |
| DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise, |
| account_info, primary_account_info, SkColor()}; |
| EXPECT_CALL(*mock_delegate(), |
| ShowSigninInterceptionBubble( |
| web_contents(), MatchBubbleParameters(expected_parameters), |
| testing::_)); |
| MaybeIntercept(account_info.account_id); |
| histogram_tester.ExpectTotalCount("Signin.Intercept.AccountInfoFetchDuration", |
| 1); |
| histogram_tester.ExpectUniqueSample( |
| "Signin.Intercept.HeuristicOutcome", |
| SigninInterceptionHeuristicOutcome::kInterceptEnterprise, 1); |
| } |
| |
| TEST_F(DiceWebSigninInterceptorTest, MultiUserInterception) { |
| base::HistogramTester histogram_tester; |
| AccountInfo primary_account_info = |
| identity_test_env()->MakeUnconsentedPrimaryAccountAvailable( |
| "bob@example.com"); |
| AccountInfo account_info = |
| identity_test_env()->MakeAccountAvailable("alice@example.com"); |
| MakeValidAccountInfo(&account_info); |
| identity_test_env()->UpdateAccountInfoForAccount(account_info); |
| |
| // Account info is already available, interception happens immediately. |
| DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { |
| DiceWebSigninInterceptor::SigninInterceptionType::kMultiUser, |
| account_info, primary_account_info, SkColor()}; |
| EXPECT_CALL(*mock_delegate(), |
| ShowSigninInterceptionBubble( |
| web_contents(), MatchBubbleParameters(expected_parameters), |
| testing::_)); |
| MaybeIntercept(account_info.account_id); |
| histogram_tester.ExpectUniqueSample( |
| "Signin.Intercept.HeuristicOutcome", |
| SigninInterceptionHeuristicOutcome::kInterceptMultiUser, 1); |
| } |