blob: 0c2ada82a404149dc1fbeb9d73e78416d293b535 [file]
// 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);
}