blob: 42e98f09b17ac25d31c2bdd6b6474b2d6801be5f [file] [log] [blame]
// Copyright 2013 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 <algorithm>
#include <cstring>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include "base/macros.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/scoped_observer.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/time/time.h"
#include "base/timer/mock_timer.h"
#include "build/build_config.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/signin/core/browser/account_reconcilor.h"
#include "components/signin/core/browser/account_tracker_service.h"
#include "components/signin/core/browser/fake_gaia_cookie_manager_service.h"
#include "components/signin/core/browser/fake_profile_oauth2_token_service.h"
#include "components/signin/core/browser/fake_signin_manager.h"
#include "components/signin/core/browser/mirror_account_reconcilor_delegate.h"
#include "components/signin/core/browser/profile_management_switches.h"
#include "components/signin/core/browser/profile_oauth2_token_service.h"
#include "components/signin/core/browser/signin_buildflags.h"
#include "components/signin/core/browser/signin_manager.h"
#include "components/signin/core/browser/signin_metrics.h"
#include "components/signin/core/browser/signin_pref_names.h"
#include "components/signin/core/browser/test_signin_client.h"
#include "components/sync_preferences/pref_service_syncable.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/gaia_urls.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "net/url_request/test_url_fetcher_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
#include "components/signin/core/browser/dice_account_reconcilor_delegate.h"
#endif
namespace {
#if defined(OS_CHROMEOS)
using FakeSigninManagerForTesting = FakeSigninManagerBase;
#else
using FakeSigninManagerForTesting = FakeSigninManager;
#endif
// TestSigninClient keeping track of the dice migration.
class DiceTestSigninClient : public TestSigninClient {
public:
explicit DiceTestSigninClient(PrefService* prefs) : TestSigninClient(prefs) {}
void SetReadyForDiceMigration(bool ready) override {
is_ready_for_dice_migration_ = ready;
}
bool is_ready_for_dice_migration() { return is_ready_for_dice_migration_; }
private:
bool is_ready_for_dice_migration_ = false;
DISALLOW_COPY_AND_ASSIGN(DiceTestSigninClient);
};
// An AccountReconcilorDelegate that records all calls (Spy pattern).
class SpyReconcilorDelegate : public signin::AccountReconcilorDelegate {
public:
int num_reconcile_finished_calls_{0};
int num_reconcile_timeout_calls_{0};
bool IsReconcileEnabled() const override { return true; }
bool IsAccountConsistencyEnforced() const override { return true; }
std::string GetGaiaApiSource() const override { return "TestSource"; }
bool ShouldAbortReconcileIfPrimaryHasError() const override { return true; }
std::string GetFirstGaiaAccountForReconcile(
const std::vector<std::string>& chrome_accounts,
const std::vector<gaia::ListedAccount>& gaia_accounts,
const std::string& primary_account,
bool first_execution,
bool will_logout) const override {
return primary_account;
}
void OnReconcileFinished(const std::string& first_account,
bool is_reconcile_noop) override {
++num_reconcile_finished_calls_;
}
base::TimeDelta GetReconcileTimeout() const override {
// Does not matter as long as it is different from base::TimeDelta::Max().
return base::TimeDelta::FromMinutes(100);
}
void OnReconcileError(const GoogleServiceAuthError& error) override {
++num_reconcile_timeout_calls_;
}
};
// gmock does not allow mocking classes with move-only parameters, preventing
// from mocking the AccountReconcilor class directly (because of the
// unique_ptr<AccountReconcilorDelegate> parameter).
// Introduce a dummy class creating the delegate internally, to avoid the move.
class DummyAccountReconcilorWithDelegate : public AccountReconcilor {
public:
explicit DummyAccountReconcilorWithDelegate(
ProfileOAuth2TokenService* token_service,
SigninManagerBase* signin_manager,
SigninClient* client,
GaiaCookieManagerService* cookie_manager_service,
signin::AccountConsistencyMethod account_consistency)
: AccountReconcilor(
token_service,
signin_manager,
client,
cookie_manager_service,
CreateAccountReconcilorDelegate(client,
signin_manager,
account_consistency)) {
Initialize(false /* start_reconcile_if_tokens_available */);
}
// Takes ownership of |delegate|.
// gmock can't work with move only parameters.
explicit DummyAccountReconcilorWithDelegate(
ProfileOAuth2TokenService* token_service,
SigninManagerBase* signin_manager,
SigninClient* client,
GaiaCookieManagerService* cookie_manager_service,
signin::AccountReconcilorDelegate* delegate)
: AccountReconcilor(
token_service,
signin_manager,
client,
cookie_manager_service,
std::unique_ptr<signin::AccountReconcilorDelegate>(delegate)) {
Initialize(false /* start_reconcile_if_tokens_available */);
}
static std::unique_ptr<signin::AccountReconcilorDelegate>
CreateAccountReconcilorDelegate(
SigninClient* signin_client,
SigninManagerBase* signin_manager,
signin::AccountConsistencyMethod account_consistency) {
switch (account_consistency) {
case signin::AccountConsistencyMethod::kMirror:
return std::make_unique<signin::MirrorAccountReconcilorDelegate>(
signin_manager);
case signin::AccountConsistencyMethod::kDisabled:
case signin::AccountConsistencyMethod::kDiceFixAuthErrors:
return std::make_unique<signin::AccountReconcilorDelegate>();
case signin::AccountConsistencyMethod::kDicePrepareMigration:
case signin::AccountConsistencyMethod::kDiceMigration:
case signin::AccountConsistencyMethod::kDice:
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
return std::make_unique<signin::DiceAccountReconcilorDelegate>(
signin_client, account_consistency);
#else
NOTREACHED();
return nullptr;
#endif
}
NOTREACHED();
return nullptr;
}
};
class MockAccountReconcilor
: public testing::StrictMock<DummyAccountReconcilorWithDelegate> {
public:
explicit MockAccountReconcilor(
ProfileOAuth2TokenService* token_service,
SigninManagerBase* signin_manager,
SigninClient* client,
GaiaCookieManagerService* cookie_manager_service,
signin::AccountConsistencyMethod account_consistency);
explicit MockAccountReconcilor(
ProfileOAuth2TokenService* token_service,
SigninManagerBase* signin_manager,
SigninClient* client,
GaiaCookieManagerService* cookie_manager_service,
std::unique_ptr<signin::AccountReconcilorDelegate> delegate);
MOCK_METHOD1(PerformMergeAction, void(const std::string& account_id));
MOCK_METHOD0(PerformLogoutAllAccountsAction, void());
};
MockAccountReconcilor::MockAccountReconcilor(
ProfileOAuth2TokenService* token_service,
SigninManagerBase* signin_manager,
SigninClient* client,
GaiaCookieManagerService* cookie_manager_service,
signin::AccountConsistencyMethod account_consistency)
: testing::StrictMock<DummyAccountReconcilorWithDelegate>(
token_service,
signin_manager,
client,
cookie_manager_service,
account_consistency) {}
MockAccountReconcilor::MockAccountReconcilor(
ProfileOAuth2TokenService* token_service,
SigninManagerBase* signin_manager,
SigninClient* client,
GaiaCookieManagerService* cookie_manager_service,
std::unique_ptr<signin::AccountReconcilorDelegate> delegate)
: testing::StrictMock<DummyAccountReconcilorWithDelegate>(
token_service,
signin_manager,
client,
cookie_manager_service,
delegate.release()) {}
} // namespace
class AccountReconcilorTest : public ::testing::Test {
public:
AccountReconcilorTest();
~AccountReconcilorTest() override;
FakeSigninManagerForTesting* signin_manager() { return &signin_manager_; }
FakeProfileOAuth2TokenService* token_service() { return &token_service_; }
DiceTestSigninClient* test_signin_client() { return &test_signin_client_; }
AccountTrackerService* account_tracker() { return &account_tracker_; }
FakeGaiaCookieManagerService* cookie_manager_service() {
return &cookie_manager_service_;
}
base::HistogramTester* histogram_tester() { return &histogram_tester_; }
MockAccountReconcilor* GetMockReconcilor();
MockAccountReconcilor* GetMockReconcilor(
std::unique_ptr<signin::AccountReconcilorDelegate> delegate);
std::string ConnectProfileToAccount(const std::string& gaia_id,
const std::string& username);
std::string PickAccountIdForAccount(const std::string& gaia_id,
const std::string& username);
void SimulateAddAccountToCookieCompleted(
GaiaCookieManagerService::Observer* observer,
const std::string& account_id,
const GoogleServiceAuthError& error);
void SimulateCookieContentSettingsChanged(
content_settings::Observer* observer,
const ContentSettingsPattern& primary_pattern);
GURL get_check_connection_info_url() {
return get_check_connection_info_url_;
}
void SetAccountConsistency(signin::AccountConsistencyMethod method);
PrefService* pref_service() { return &pref_service_; }
private:
base::MessageLoop loop;
signin::AccountConsistencyMethod account_consistency_;
sync_preferences::TestingPrefServiceSyncable pref_service_;
FakeProfileOAuth2TokenService token_service_;
DiceTestSigninClient test_signin_client_;
AccountTrackerService account_tracker_;
FakeGaiaCookieManagerService cookie_manager_service_;
FakeSigninManagerForTesting signin_manager_;
std::unique_ptr<MockAccountReconcilor> mock_reconcilor_;
base::HistogramTester histogram_tester_;
GURL get_check_connection_info_url_;
DISALLOW_COPY_AND_ASSIGN(AccountReconcilorTest);
};
// For tests that must be run with multiple account consistency methods.
class AccountReconcilorMethodParamTest
: public AccountReconcilorTest,
public ::testing::WithParamInterface<signin::AccountConsistencyMethod> {
public:
AccountReconcilorMethodParamTest() = default;
private:
DISALLOW_COPY_AND_ASSIGN(AccountReconcilorMethodParamTest);
};
INSTANTIATE_TEST_CASE_P(Dice_Mirror,
AccountReconcilorMethodParamTest,
::testing::Values(
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
signin::AccountConsistencyMethod::kDice,
#endif
signin::AccountConsistencyMethod::kMirror));
AccountReconcilorTest::AccountReconcilorTest()
: account_consistency_(signin::AccountConsistencyMethod::kDisabled),
test_signin_client_(&pref_service_),
cookie_manager_service_(&token_service_,
GaiaConstants::kChromeSource,
&test_signin_client_),
#if defined(OS_CHROMEOS)
signin_manager_(&test_signin_client_, &account_tracker_) {
#else
signin_manager_(&test_signin_client_,
&token_service_,
&account_tracker_,
&cookie_manager_service_) {
#endif
AccountTrackerService::RegisterPrefs(pref_service_.registry());
SigninManagerBase::RegisterProfilePrefs(pref_service_.registry());
SigninManagerBase::RegisterPrefs(pref_service_.registry());
pref_service_.registry()->RegisterBooleanPref(
prefs::kTokenServiceDiceCompatible, false);
get_check_connection_info_url_ =
GaiaUrls::GetInstance()->GetCheckConnectionInfoURLWithSource(
GaiaConstants::kChromeSource);
account_tracker_.Initialize(&test_signin_client_);
cookie_manager_service_.SetListAccountsResponseHttpNotFound();
signin_manager_.Initialize(nullptr);
// The reconcilor should not be built before the test can set the account
// consistency method.
EXPECT_FALSE(mock_reconcilor_);
}
MockAccountReconcilor* AccountReconcilorTest::GetMockReconcilor() {
if (!mock_reconcilor_) {
mock_reconcilor_ = std::make_unique<MockAccountReconcilor>(
&token_service_, &signin_manager_, &test_signin_client_,
&cookie_manager_service_, account_consistency_);
}
return mock_reconcilor_.get();
}
MockAccountReconcilor* AccountReconcilorTest::GetMockReconcilor(
std::unique_ptr<signin::AccountReconcilorDelegate> delegate) {
mock_reconcilor_ = std::make_unique<MockAccountReconcilor>(
&token_service_, &signin_manager_, &test_signin_client_,
&cookie_manager_service_, std::move(delegate));
return mock_reconcilor_.get();
}
AccountReconcilorTest::~AccountReconcilorTest() {
if (mock_reconcilor_)
mock_reconcilor_->Shutdown();
signin_manager_.Shutdown();
cookie_manager_service_.Shutdown();
account_tracker_.Shutdown();
test_signin_client_.Shutdown();
token_service_.Shutdown();
}
std::string AccountReconcilorTest::ConnectProfileToAccount(
const std::string& gaia_id,
const std::string& username) {
const std::string account_id = PickAccountIdForAccount(gaia_id, username);
#if !defined(OS_CHROMEOS)
signin_manager()->set_password("password");
#endif
signin_manager()->SetAuthenticatedAccountInfo(gaia_id, username);
token_service()->UpdateCredentials(account_id, "refresh_token");
return account_id;
}
std::string AccountReconcilorTest::PickAccountIdForAccount(
const std::string& gaia_id,
const std::string& username) {
return account_tracker()->PickAccountIdForAccount(gaia_id, username);
}
void AccountReconcilorTest::SimulateAddAccountToCookieCompleted(
GaiaCookieManagerService::Observer* observer,
const std::string& account_id,
const GoogleServiceAuthError& error) {
observer->OnAddAccountToCookieCompleted(account_id, error);
}
void AccountReconcilorTest::SimulateCookieContentSettingsChanged(
content_settings::Observer* observer,
const ContentSettingsPattern& primary_pattern) {
observer->OnContentSettingChanged(
primary_pattern, ContentSettingsPattern::Wildcard(),
CONTENT_SETTINGS_TYPE_COOKIES, std::string());
}
void AccountReconcilorTest::SetAccountConsistency(
signin::AccountConsistencyMethod method) {
account_consistency_ = method;
}
TEST_F(AccountReconcilorTest, Basic) {
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
}
#if !defined(OS_CHROMEOS)
// This method requires the use of the |TestSigninClient| to be created from the
// |ChromeSigninClientFactory| because it overrides the |GoogleSigninSucceeded|
// method with an empty implementation. On MacOS, the normal implementation
// causes the try_bots to time out.
TEST_F(AccountReconcilorTest, SigninManagerRegistration) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
ASSERT_FALSE(reconcilor->IsRegisteredWithTokenService());
account_tracker()->SeedAccountInfo("12345", "user@gmail.com");
signin_manager()->SignIn("12345", "user@gmail.com", "password");
ASSERT_TRUE(reconcilor->IsRegisteredWithTokenService());
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction());
signin_manager()->SignOut(signin_metrics::SIGNOUT_TEST,
signin_metrics::SignoutDelete::IGNORE_METRIC);
ASSERT_FALSE(reconcilor->IsRegisteredWithTokenService());
}
// This method requires the use of the |TestSigninClient| to be created from the
// |ChromeSigninClientFactory| because it overrides the |GoogleSigninSucceeded|
// method with an empty implementation. On MacOS, the normal implementation
// causes the try_bots to time out.
TEST_F(AccountReconcilorTest, Reauth) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string email = "user@gmail.com";
const std::string account_id = ConnectProfileToAccount("12345", email);
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
ASSERT_TRUE(reconcilor->IsRegisteredWithTokenService());
// Simulate reauth. The state of the reconcilor should not change.
signin_manager()->OnExternalSigninCompleted(email);
ASSERT_TRUE(reconcilor->IsRegisteredWithTokenService());
}
#endif // !defined(OS_CHROMEOS)
TEST_F(AccountReconcilorTest, ProfileAlreadyConnected) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
ConnectProfileToAccount("12345", "user@gmail.com");
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
ASSERT_TRUE(reconcilor->IsRegisteredWithTokenService());
}
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
struct AccountReconcilorTestDiceParam {
const char* tokens;
const char* cookies;
bool is_first_reconcile;
const char* gaia_api_calls;
const char* tokens_after_reconcile;
const char* cookies_after_reconcile;
};
// Pretty prints a AccountReconcilorTestDiceParam. Used by gtest.
void PrintTo(const AccountReconcilorTestDiceParam& param, ::std::ostream* os) {
*os << "Tokens: " << param.tokens << ". Cookies: " << param.cookies
<< ". First reconcile: " << param.is_first_reconcile;
}
// clang-format off
const AccountReconcilorTestDiceParam kDiceParams[] = {
// This table encodes the initial state and expectations of a reconcile.
// The syntax is:
// - Tokens:
// A, B, C: Accounts for which we have a token in Chrome.
// *: The next account is the main Chrome account (i.e. in SigninManager).
// x: The next account has a token error.
// - Cookies:
// A, B, C: Accounts in the Gaia cookie (returned by ListAccounts).
// x: The next cookie is marked "invalid".
// - First Run: true if this is the first reconcile (i.e. Chrome startup).
// - API calls:
// X: Logout all accounts.
// A, B, C: Merge account.
// -------------------------------------------------------------------------
// Tokens | Cookies | First Run | Gaia calls | Tokens after | Cookies after
// -------------------------------------------------------------------------
// First reconcile (Chrome restart): Rebuild the Gaia cookie to match the
// tokens. Make the Sync account the default account in the Gaia cookie.
// Sync enabled.
{ "*AB", "AB", true, "", "*AB", "AB"},
{ "*AB", "BA", true, "XAB", "*AB", "AB"},
{ "*AB", "A", true, "B", "*AB", "AB"},
{ "*AB", "B", true, "XAB", "*AB", "AB"},
{ "*AB", "", true, "AB", "*AB", "AB"},
// Sync enabled, token error on primary.
{ "*xAB", "AB", true, "X", "*xA", ""},
{ "*xAB", "BA", true, "XB", "*xAB", "B"},
{ "*xAB", "A", true, "X", "*xA", ""},
{ "*xAB", "B", true, "", "*xAB", "B"},
{ "*xAB", "", true, "B", "*xAB", "B"},
// Sync enabled, token error on secondary.
{ "*AxB", "AB", true, "XA", "*A", "A"},
{ "*AxB", "BA", true, "XA", "*A", "A"},
{ "*AxB", "A", true, "", "*A", "A"},
{ "*AxB", "B", true, "XA", "*A", "A"},
{ "*AxB", "", true, "A", "*A", "A"},
// Sync enabled, token error on both accounts.
{ "*xAxB", "AB", true, "X", "*xA", ""},
{ "*xAxB", "BA", true, "X", "*xA", ""},
{ "*xAxB", "A", true, "X", "*xA", ""},
{ "*xAxB", "B", true, "X", "*xA", ""},
{ "*xAxB", "", true, "", "*xA", ""},
// Sync disabled.
{ "AB", "AB", true, "", "AB", "AB"},
{ "AB", "BA", true, "", "AB", "BA"},
{ "AB", "A", true, "B", "AB", "AB"},
{ "AB", "B", true, "A", "AB", "BA"},
{ "AB", "", true, "AB", "AB", "AB"},
// Sync disabled, token error on first account.
{ "xAB", "AB", true, "XB", "B", "B"},
{ "xAB", "BA", true, "XB", "B", "B"},
{ "xAB", "A", true, "XB", "B", "B"},
{ "xAB", "B", true, "", "B", "B"},
{ "xAB", "", true, "B", "B", "B"},
// Sync disabled, token error on second account .
{ "AxB", "AB", true, "XA", "A", "A"},
{ "AxB", "BA", true, "XA", "A", "A"},
{ "AxB", "A", true, "", "A", "A"},
{ "AxB", "B", true, "XA", "A", "A"},
{ "AxB", "", true, "A", "A", "A"},
// Sync disabled, token error on both accounts.
{ "xAxB", "AB", true, "X", "", ""},
{ "xAxB", "BA", true, "X", "", ""},
{ "xAxB", "A", true, "X", "", ""},
{ "xAxB", "B", true, "X", "", ""},
{ "xAxB", "", true, "", "", ""},
// Chrome is running: Do not change the order of accounts already present in
// the Gaia cookies.
// Sync enabled.
{ "*AB", "AB", false, "", "*AB", "AB"},
{ "*AB", "BA", false, "", "*AB", "BA"},
{ "*AB", "A", false, "B", "*AB", "AB"},
{ "*AB", "B", false, "A", "*AB", "BA"},
{ "*AB", "", false, "AB", "*AB", "AB"},
// Sync enabled, token error on primary.
{ "*xAB", "AB", false, "X", "*xA", ""},
{ "*xAB", "BA", false, "XB", "*xAB", "B"},
{ "*xAB", "A", false, "X", "*xA", ""},
{ "*xAB", "B", false, "", "*xAB", "B"},
{ "*xAB", "", false, "B", "*xAB", "B"},
// Sync enabled, token error on secondary.
{ "*AxB", "AB", false, "XA", "*A", "A"},
{ "*AxB", "BA", false, "XA", "*A", "A"},
{ "*AxB", "A", false, "", "*A", "A"},
{ "*AxB", "B", false, "XA", "*A", "A"},
{ "*AxB", "", false, "A", "*A", "A"},
// Sync enabled, token error on both accounts.
{ "*xAxB", "AB", false, "X", "*xA", ""},
{ "*xAxB", "BA", false, "X", "*xA", ""},
{ "*xAxB", "A", false, "X", "*xA", ""},
{ "*xAxB", "B", false, "X", "*xA", ""},
{ "*xAxB", "", false, "", "*xA", ""},
// Sync disabled.
{ "AB", "AB", false, "", "AB", "AB"},
{ "AB", "BA", false, "", "AB", "BA"},
{ "AB", "A", false, "B", "AB", "AB"},
{ "AB", "B", false, "A", "AB", "BA"},
{ "AB", "", false, "AB", "AB", "AB"},
// Sync disabled, token error on first account.
{ "xAB", "AB", false, "X", "", ""},
{ "xAB", "BA", false, "XB", "B", "B"},
{ "xAB", "A", false, "X", "", ""},
{ "xAB", "B", false, "", "B", "B"},
{ "xAB", "", false, "B", "B", "B"},
// Sync disabled, token error on second account.
{ "AxB", "AB", false, "XA", "A", "A"},
{ "AxB", "BA", false, "X", "", ""},
{ "AxB", "A", false, "", "A", "A"},
{ "AxB", "B", false, "X", "", ""},
{ "AxB", "", false, "A", "A", "A"},
// Sync disabled, token error on both accounts.
{ "xAxB", "AB", false, "X", "", ""},
{ "xAxB", "BA", false, "X", "", ""},
{ "xAxB", "A", false, "X", "", ""},
{ "xAxB", "B", false, "X", "", ""},
{ "xAxB", "", false, "", "", ""},
// Account marked as invalid in cookies.
// Do not logout. Regression tests for http://crbug.com/854799
{ "", "xA", false, "", "", "xA"},
{ "", "xAxB", false, "", "", "xAxB"},
{ "xA", "xA", false, "", "", "xA"},
{ "xAB", "xAB", false, "", "B", "xAB"},
{ "AxB", "AxC", false, "", "A", "AxC"},
{ "B", "xAB", false, "", "B", "xAB"},
{ "*xA", "xA", false, "", "*xA", "xA"},
{ "*xA", "xB", false, "", "*xA", "xB"},
{ "*xAB", "xAB", false, "", "*xAB", "xAB"},
// Appending a new cookie after the invalid one.
{ "B", "xA", false, "B", "B", "xAB"},
{ "xAB", "xA", false, "B", "B", "xAB"},
// Cookies can be refreshed in pace, without logout.
{ "AB", "xAB", false, "A", "AB", "AB"},
{ "*AB", "xBxA", false, "BA", "*AB", "BA"},
// Logout when necessary.
{ "", "xAB", false, "X", "", ""},
{ "xAB", "xAC", false, "X", "", ""},
{ "xAB", "AxC", false, "X", "", ""},
{ "*xAB", "xABC", false, "X", "*xA", ""},
{ "xAB", "xABC", false, "X", "", ""},
// Miscellaneous cases.
// Check that unknown Gaia accounts are signed out.
{ "", "A", true, "X", "", ""},
{ "", "A", false, "X", "", ""},
{ "*A", "AB", true, "XA", "*A", "A"},
{ "*A", "AB", false, "XA", "*A", "A"},
// Check that Gaia default account is kept in first position.
{ "AB", "BC", true, "XBA", "AB", "BA"},
{ "AB", "BC", false, "XBA", "AB", "BA"},
// Required for idempotency check.
{ "", "", false, "", "", ""},
{ "*A", "A", false, "", "*A", "A"},
{ "A", "A", false, "", "A", "A"},
{ "B", "B", false, "", "B", "B"},
{ "*xA", "", false, "", "*xA", ""},
{ "*xAB", "B", false, "", "*xAB", "B"},
{ "A", "AxC", false, "", "A", "AxC"},
};
// clang-format on
// Parameterized version of AccountReconcilorTest.
class AccountReconcilorTestDice
: public AccountReconcilorTest,
public ::testing::WithParamInterface<AccountReconcilorTestDiceParam> {
protected:
struct Account {
std::string email;
std::string gaia_id;
};
struct Token {
std::string gaia_id;
std::string email;
bool is_authenticated;
bool has_error;
};
struct Cookie {
std::string gaia_id;
bool is_valid;
bool operator==(const Cookie& other) const {
return gaia_id == other.gaia_id && is_valid == other.is_valid;
}
};
AccountReconcilorTestDice() {
accounts_['A'] = {"a@gmail.com", "A"};
accounts_['B'] = {"b@gmail.com", "B"};
accounts_['C'] = {"c@gmail.com", "C"};
}
// Build Tokens from string.
std::vector<Token> ParseTokenString(const char* token_string) {
std::vector<Token> parsed_tokens;
bool is_authenticated = false;
bool has_error = false;
for (int i = 0; token_string[i] != '\0'; ++i) {
char token_code = token_string[i];
if (token_code == '*') {
is_authenticated = true;
continue;
}
if (token_code == 'x') {
has_error = true;
continue;
}
parsed_tokens.push_back({accounts_[token_code].gaia_id,
accounts_[token_code].email, is_authenticated,
has_error});
is_authenticated = false;
has_error = false;
}
return parsed_tokens;
}
// Build Cookies from string.
std::vector<Cookie> ParseCookieString(const char* cookie_string) {
std::vector<Cookie> parsed_cookies;
bool valid = true;
for (int i = 0; cookie_string[i] != '\0'; ++i) {
char cookie_code = cookie_string[i];
if (cookie_code == 'x') {
valid = false;
continue;
}
parsed_cookies.push_back({accounts_[cookie_code].gaia_id, valid});
valid = true;
}
return parsed_cookies;
}
// Checks that the tokens in the TokenService match the tokens.
void VerifyCurrentTokens(const std::vector<Token>& tokens) {
EXPECT_EQ(token_service()->GetAccounts().size(), tokens.size());
bool authenticated_account_found = false;
for (const Token& token : tokens) {
std::string account_id =
PickAccountIdForAccount(token.gaia_id, token.email);
EXPECT_TRUE(token_service()->RefreshTokenIsAvailable(account_id));
EXPECT_EQ(token.has_error,
token_service()->RefreshTokenHasError(account_id));
if (token.is_authenticated) {
EXPECT_EQ(account_id, signin_manager()->GetAuthenticatedAccountId());
authenticated_account_found = true;
}
}
if (!authenticated_account_found)
EXPECT_EQ("", signin_manager()->GetAuthenticatedAccountId());
}
// Checks that reconcile is idempotent.
void CheckReconcileIdempotent(const AccountReconcilorTestDiceParam& param) {
// Simulate another reconcile based on the results of this one: find the
// corresponding row in the table and check that it does nothing.
for (const AccountReconcilorTestDiceParam& row : kDiceParams) {
if (row.is_first_reconcile)
continue;
if (strcmp(row.tokens, param.tokens_after_reconcile) != 0)
continue;
if (strcmp(row.cookies, param.cookies_after_reconcile) != 0)
continue;
EXPECT_STREQ(row.tokens, row.tokens_after_reconcile);
EXPECT_STREQ(row.cookies, row.cookies_after_reconcile);
return;
}
ADD_FAILURE() << "Could not check that reconcile is idempotent.";
}
void ConfigureCookieManagerService(const std::vector<Cookie>& cookies) {
std::vector<FakeGaiaCookieManagerService::CookieParams> cookie_params;
for (const auto& cookie : cookies) {
std::string gaia_id = cookie.gaia_id;
cookie_params.push_back({accounts_[gaia_id[0]].email, gaia_id,
cookie.is_valid, false /* signed_out */,
true /* verified */});
}
cookie_manager_service()->SetListAccountsResponseWithParams(cookie_params);
cookie_manager_service()->set_list_accounts_stale_for_testing(true);
}
std::map<char, Account> accounts_;
};
// Checks one row of the kDiceParams table above.
TEST_P(AccountReconcilorTestDice, TableRowTest) {
// Enable Dice.
SetAccountConsistency(signin::AccountConsistencyMethod::kDice);
// Check that reconcile is idempotent: when called twice in a row it should do
// nothing on the second call.
CheckReconcileIdempotent(GetParam());
// Setup tokens.
std::vector<Token> tokens_before_reconcile =
ParseTokenString(GetParam().tokens);
for (const Token& token : tokens_before_reconcile) {
std::string account_id =
PickAccountIdForAccount(token.gaia_id, token.email);
if (token.is_authenticated)
ConnectProfileToAccount(token.gaia_id, token.email);
else
token_service()->UpdateCredentials(account_id, "refresh_token");
if (token.has_error) {
token_service()->UpdateAuthErrorForTesting(
account_id, GoogleServiceAuthError(
GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
}
}
VerifyCurrentTokens(tokens_before_reconcile);
// Setup cookies.
std::vector<Cookie> cookies = ParseCookieString(GetParam().cookies);
ConfigureCookieManagerService(cookies);
// Call list accounts now so that the next call completes synchronously.
cookie_manager_service()->ListAccounts(nullptr, nullptr, "foo");
base::RunLoop().RunUntilIdle();
// Setup expectations.
testing::InSequence mock_sequence;
bool logout_action = false;
for (int i = 0; GetParam().gaia_api_calls[i] != '\0'; ++i) {
if (GetParam().gaia_api_calls[i] == 'X') {
logout_action = true;
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction())
.Times(1);
cookies.clear();
continue;
}
std::string cookie(1, GetParam().gaia_api_calls[i]);
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(cookie)).Times(1);
// MergeSession fixes an existing cookie or appends it at the end.
std::vector<Cookie>::iterator it = std::find(
cookies.begin(), cookies.end(), Cookie{cookie, false /* is_valid */});
if (it == cookies.end())
cookies.push_back({cookie, true});
else
it->is_valid = true;
}
if (!logout_action) {
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction())
.Times(0);
}
// Check the expected cookies after reconcile.
std::vector<Cookie> expected_cookies =
ParseCookieString(GetParam().cookies_after_reconcile);
ASSERT_EQ(expected_cookies, cookies);
// Reconcile.
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor->first_execution_);
reconcilor->first_execution_ = GetParam().is_first_reconcile;
reconcilor->StartReconcile();
for (int i = 0; GetParam().gaia_api_calls[i] != '\0'; ++i) {
if (GetParam().gaia_api_calls[i] == 'X')
continue;
std::string account_id =
PickAccountIdForAccount(accounts_[GetParam().gaia_api_calls[i]].gaia_id,
accounts_[GetParam().gaia_api_calls[i]].email);
SimulateAddAccountToCookieCompleted(
reconcilor, account_id, GoogleServiceAuthError::AuthErrorNone());
}
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
VerifyCurrentTokens(ParseTokenString(GetParam().tokens_after_reconcile));
testing::Mock::VerifyAndClearExpectations(GetMockReconcilor());
// Another reconcile is sometimes triggered if Chrome accounts have changed.
// Allow it to finish.
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(testing::_))
.WillRepeatedly(testing::Return());
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction())
.WillRepeatedly(testing::Return());
ConfigureCookieManagerService({});
base::RunLoop().RunUntilIdle();
}
INSTANTIATE_TEST_CASE_P(DiceTable,
AccountReconcilorTestDice,
::testing::ValuesIn(kDiceParams));
// Tests that the AccountReconcilor is always registered.
TEST_F(AccountReconcilorTest, DiceTokenServiceRegistration) {
SetAccountConsistency(signin::AccountConsistencyMethod::kDice);
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
ASSERT_TRUE(reconcilor->IsRegisteredWithTokenService());
account_tracker()->SeedAccountInfo("12345", "user@gmail.com");
signin_manager()->SignIn("12345", "user@gmail.com", "password");
ASSERT_TRUE(reconcilor->IsRegisteredWithTokenService());
// Reconcilor should not logout all accounts from the cookies when
// SigninManager signs out.
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction()).Times(0);
signin_manager()->SignOut(signin_metrics::SIGNOUT_TEST,
signin_metrics::SignoutDelete::IGNORE_METRIC);
ASSERT_TRUE(reconcilor->IsRegisteredWithTokenService());
}
// Tests that reconcile starts even when Sync is not enabled.
TEST_F(AccountReconcilorTest, DiceReconcileWhithoutSignin) {
// Enable Dice.
SetAccountConsistency(signin::AccountConsistencyMethod::kDice);
// Add a token in Chrome but do not sign in.
const std::string account_id =
PickAccountIdForAccount("12345", "user@gmail.com");
token_service()->UpdateCredentials(account_id, "refresh_token");
cookie_manager_service()->SetListAccountsResponseNoAccounts();
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id));
AccountReconcilor* reconcilor = GetMockReconcilor();
reconcilor->StartReconcile();
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
SimulateAddAccountToCookieCompleted(reconcilor, account_id,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
}
// Checks that nothing happens when there is no Chrome account and no Gaia
// cookie.
TEST_F(AccountReconcilorTest, DiceReconcileNoop) {
// Enable Dice.
SetAccountConsistency(signin::AccountConsistencyMethod::kDice);
// No Chrome account and no cookie.
cookie_manager_service()->SetListAccountsResponseNoAccounts();
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(testing::_)).Times(0);
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction()).Times(0);
AccountReconcilor* reconcilor = GetMockReconcilor();
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
}
// Tests that the first Gaia account is re-used when possible.
TEST_F(AccountReconcilorTest, DiceReconcileReuseGaiaFirstAccount) {
// Enable Dice.
SetAccountConsistency(signin::AccountConsistencyMethod::kDice);
// Add accounts 1 and 2 to the token service.
const std::string account_id_1 =
PickAccountIdForAccount("12345", "user@gmail.com");
const std::string account_id_2 =
PickAccountIdForAccount("67890", "other@gmail.com");
token_service()->UpdateCredentials(account_id_1, "refresh_token");
token_service()->UpdateCredentials(account_id_2, "refresh_token");
ASSERT_EQ(2u, token_service()->GetAccounts().size());
ASSERT_EQ(account_id_1, token_service()->GetAccounts()[0]);
ASSERT_EQ(account_id_2, token_service()->GetAccounts()[1]);
// Add accounts 2 and 3 to the Gaia cookie.
const std::string account_id_3 =
PickAccountIdForAccount("9999", "foo@gmail.com");
cookie_manager_service()->SetListAccountsResponseTwoAccounts(
"other@gmail.com", "67890", "foo@gmail.com", "9999");
testing::InSequence mock_sequence;
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction());
// Account 2 is added first.
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id_2));
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id_1));
AccountReconcilor* reconcilor = GetMockReconcilor();
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
SimulateAddAccountToCookieCompleted(reconcilor, account_id_1,
GoogleServiceAuthError::AuthErrorNone());
SimulateAddAccountToCookieCompleted(reconcilor, account_id_2,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
}
// Tests that the first account is kept in cache and reused when cookies are
// lost.
TEST_F(AccountReconcilorTest, DiceLastKnownFirstAccount) {
// Enable Dice.
SetAccountConsistency(signin::AccountConsistencyMethod::kDice);
// Add accounts to the token service and the Gaia cookie in a different order.
const std::string account_id_1 =
PickAccountIdForAccount("12345", "user@gmail.com");
const std::string account_id_2 =
PickAccountIdForAccount("67890", "other@gmail.com");
cookie_manager_service()->SetListAccountsResponseTwoAccounts(
"other@gmail.com", "67890", "user@gmail.com", "12345");
token_service()->UpdateCredentials(account_id_1, "refresh_token");
token_service()->UpdateCredentials(account_id_2, "refresh_token");
ASSERT_EQ(2u, token_service()->GetAccounts().size());
ASSERT_EQ(account_id_1, token_service()->GetAccounts()[0]);
ASSERT_EQ(account_id_2, token_service()->GetAccounts()[1]);
// Do one reconcile. It should do nothing but to populating the last known
// account.
{
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(testing::_)).Times(0);
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction())
.Times(0);
AccountReconcilor* reconcilor = GetMockReconcilor();
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
}
// Delete the cookies.
cookie_manager_service()->SetListAccountsResponseNoAccounts();
cookie_manager_service()->set_list_accounts_stale_for_testing(true);
// Reconcile again and check that account_id_2 is added first.
{
testing::InSequence mock_sequence;
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id_2))
.Times(1);
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id_1))
.Times(1);
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction())
.Times(0);
AccountReconcilor* reconcilor = GetMockReconcilor();
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
SimulateAddAccountToCookieCompleted(
reconcilor, account_id_2, GoogleServiceAuthError::AuthErrorNone());
SimulateAddAccountToCookieCompleted(
reconcilor, account_id_1, GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
}
}
// Checks that the reconcilor does not log out unverified accounts.
TEST_F(AccountReconcilorTest, UnverifiedAccountNoop) {
// Enable Dice.
SetAccountConsistency(signin::AccountConsistencyMethod::kDice);
// Add a unverified account to the Gaia cookie.
cookie_manager_service()->SetListAccountsResponseOneAccountWithParams(
{"user@gmail.com", "12345", true /* valid */, false /* signed_out */,
false /* verified */});
// Check that nothing happens.
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(testing::_)).Times(0);
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction()).Times(0);
AccountReconcilor* reconcilor = GetMockReconcilor();
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
}
// Checks that the reconcilor does not log out unverified accounts when adding
// a new account to the Gaia cookie.
TEST_F(AccountReconcilorTest, UnverifiedAccountMerge) {
// Enable Dice.
SetAccountConsistency(signin::AccountConsistencyMethod::kDice);
// Add a unverified account to the Gaia cookie.
cookie_manager_service()->SetListAccountsResponseOneAccountWithParams(
{"user@gmail.com", "12345", true /* valid */, false /* signed_out */,
false /* verified */});
// Add a token to Chrome.
const std::string chrome_account_id =
PickAccountIdForAccount("67890", "other@gmail.com");
token_service()->UpdateCredentials(chrome_account_id, "refresh_token");
// Check that the Chrome account is merged and the unverified account is not
// logged out.
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(chrome_account_id))
.Times(1);
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction()).Times(0);
AccountReconcilor* reconcilor = GetMockReconcilor();
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
SimulateAddAccountToCookieCompleted(reconcilor, chrome_account_id,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
}
// Regression test for https://crbug.com/825143
// Checks that the primary account is not signed out when it changes during the
// reconcile.
TEST_F(AccountReconcilorTest, HandleSigninDuringReconcile) {
SetAccountConsistency(
signin::AccountConsistencyMethod::kDicePrepareMigration);
cookie_manager_service()->SetListAccountsResponseNoAccounts();
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_EQ(signin::AccountReconcilorDelegate::RevokeTokenOption::kRevoke,
reconcilor->delegate_->ShouldRevokeSecondaryTokensBeforeReconcile(
std::vector<gaia::ListedAccount>()));
// Signin during reconcile.
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id)).Times(1);
base::RunLoop().RunUntilIdle();
SimulateAddAccountToCookieCompleted(reconcilor, account_id,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
// The account has not been deleted.
EXPECT_TRUE(token_service()->RefreshTokenIsAvailable(account_id));
}
// Tests that the Dice migration happens after a no-op reconcile.
TEST_F(AccountReconcilorTest, DiceMigrationAfterNoop) {
// Enable Dice migration.
SetAccountConsistency(signin::AccountConsistencyMethod::kDiceMigration);
pref_service()->SetBoolean(prefs::kTokenServiceDiceCompatible, true);
// Chrome account is consistent with the cookie.
const std::string account_id =
PickAccountIdForAccount("12345", "user@gmail.com");
token_service()->UpdateCredentials(account_id, "refresh_token");
cookie_manager_service()->SetListAccountsResponseOneAccount("user@gmail.com",
"12345");
AccountReconcilor* reconcilor = GetMockReconcilor();
// Dice is not enabled by default.
EXPECT_FALSE(reconcilor->delegate_->IsAccountConsistencyEnforced());
// No-op reconcile.
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(testing::_)).Times(0);
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction()).Times(0);
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
// Migration will happen on next startup.
EXPECT_TRUE(test_signin_client()->is_ready_for_dice_migration());
}
// Tests that the Dice no migration happens if the token service is not ready.
TEST_F(AccountReconcilorTest, DiceNoMigrationWhenTokensNotReady) {
// Enable Dice migration.
SetAccountConsistency(signin::AccountConsistencyMethod::kDiceMigration);
// Chrome account is consistent with the cookie.
const std::string account_id =
PickAccountIdForAccount("12345", "user@gmail.com");
token_service()->UpdateCredentials(account_id, "refresh_token");
cookie_manager_service()->SetListAccountsResponseOneAccount("user@gmail.com",
"12345");
AccountReconcilor* reconcilor = GetMockReconcilor();
// Dice is not enabled by default.
EXPECT_FALSE(reconcilor->delegate_->IsAccountConsistencyEnforced());
// No-op reconcile.
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(testing::_)).Times(0);
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction()).Times(0);
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
// Migration did not happen.
EXPECT_FALSE(test_signin_client()->is_ready_for_dice_migration());
}
// Tests that the Dice migration does not happen after a busy reconcile.
TEST_F(AccountReconcilorTest, DiceNoMigrationAfterReconcile) {
// Enable Dice migration.
SetAccountConsistency(signin::AccountConsistencyMethod::kDiceMigration);
pref_service()->SetBoolean(prefs::kTokenServiceDiceCompatible, true);
// Add a token in Chrome.
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
cookie_manager_service()->SetListAccountsResponseNoAccounts();
AccountReconcilor* reconcilor = GetMockReconcilor();
// Dice is not enabled by default.
EXPECT_FALSE(reconcilor->delegate_->IsAccountConsistencyEnforced());
// Busy reconcile.
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id));
reconcilor->StartReconcile();
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
SimulateAddAccountToCookieCompleted(reconcilor, account_id,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
// Migration did not happen.
EXPECT_FALSE(test_signin_client()->is_ready_for_dice_migration());
}
// Tests that secondary refresh tokens are cleared when cookie is empty during
// Dice migration.
TEST_F(AccountReconcilorTest, MigrationClearSecondaryTokens) {
// Enable Dice migration.
SetAccountConsistency(signin::AccountConsistencyMethod::kDiceMigration);
pref_service()->SetBoolean(prefs::kTokenServiceDiceCompatible, true);
// Add a tokens in Chrome, signin to Sync, but no Gaia cookies.
const std::string account_id_1 =
ConnectProfileToAccount("12345", "user@gmail.com");
const std::string account_id_2 =
PickAccountIdForAccount("67890", "other@gmail.com");
token_service()->UpdateCredentials(account_id_2, "refresh_token");
cookie_manager_service()->SetListAccountsResponseNoAccounts();
ASSERT_TRUE(token_service()->RefreshTokenIsAvailable(account_id_1));
ASSERT_TRUE(token_service()->RefreshTokenIsAvailable(account_id_2));
// Reconcile should revoke the secondary account.
AccountReconcilor* reconcilor = GetMockReconcilor();
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id_1));
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
SimulateAddAccountToCookieCompleted(reconcilor, account_id_1,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
EXPECT_TRUE(token_service()->RefreshTokenIsAvailable(account_id_1));
EXPECT_FALSE(token_service()->RefreshTokenIsAvailable(account_id_2));
// Profile was not migrated.
EXPECT_FALSE(test_signin_client()->is_ready_for_dice_migration());
}
// Tests that all refresh tokens are cleared when cookie is empty during
// Dice migration, if Sync is not enabled.
TEST_F(AccountReconcilorTest, MigrationClearAllTokens) {
// Enable Dice migration.
SetAccountConsistency(signin::AccountConsistencyMethod::kDiceMigration);
pref_service()->SetBoolean(prefs::kTokenServiceDiceCompatible, true);
// Add a tokens in Chrome but no Gaia cookies.
const std::string account_id_1 =
PickAccountIdForAccount("12345", "user@gmail.com");
const std::string account_id_2 =
PickAccountIdForAccount("67890", "other@gmail.com");
token_service()->UpdateCredentials(account_id_1, "refresh_token");
token_service()->UpdateCredentials(account_id_2, "refresh_token");
cookie_manager_service()->SetListAccountsResponseNoAccounts();
ASSERT_TRUE(token_service()->RefreshTokenIsAvailable(account_id_1));
ASSERT_TRUE(token_service()->RefreshTokenIsAvailable(account_id_2));
// Reconcile should revoke the secondary account.
AccountReconcilor* reconcilor = GetMockReconcilor();
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
EXPECT_FALSE(token_service()->RefreshTokenIsAvailable(account_id_1));
EXPECT_FALSE(token_service()->RefreshTokenIsAvailable(account_id_2));
// Profile is now ready for migration on next startup.
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(test_signin_client()->is_ready_for_dice_migration());
}
#endif // BUILDFLAG(ENABLE_DICE_SUPPORT)
// Tests that reconcile cannot start before the tokens are loaded, and is
// automatically started when tokens are loaded.
TEST_F(AccountReconcilorTest, TokensNotLoaded) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
cookie_manager_service()->SetListAccountsResponseNoAccounts();
token_service()->set_all_credentials_loaded_for_testing(false);
AccountReconcilor* reconcilor = GetMockReconcilor();
reconcilor->StartReconcile();
#if !defined(OS_CHROMEOS)
// No reconcile when tokens are not loaded, except on ChromeOS where reconcile
// can start as long as the token service is not empty.
ASSERT_FALSE(reconcilor->is_reconcile_started_);
// When tokens are loaded, reconcile starts automatically.
token_service()->LoadCredentials(account_id);
#endif
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id));
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
SimulateAddAccountToCookieCompleted(reconcilor, account_id,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
}
TEST_F(AccountReconcilorTest, GetAccountsFromCookieSuccess) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
cookie_manager_service()->SetListAccountsResponseOneAccountWithParams(
{"user@gmail.com", "12345", false /* valid */, false /* signed_out */,
true /* verified */});
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id));
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
reconcilor->StartReconcile();
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_RUNNING, reconcilor->GetState());
base::RunLoop().RunUntilIdle();
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_RUNNING, reconcilor->GetState());
std::vector<gaia::ListedAccount> accounts;
std::vector<gaia::ListedAccount> signed_out_accounts;
ASSERT_TRUE(cookie_manager_service()->ListAccounts(
&accounts, &signed_out_accounts, GaiaConstants::kChromeSource));
ASSERT_EQ(1u, accounts.size());
ASSERT_EQ(account_id, accounts[0].id);
ASSERT_EQ(0u, signed_out_accounts.size());
}
TEST_F(AccountReconcilorTest, GetAccountsFromCookieFailure) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
ConnectProfileToAccount("12345", "user@gmail.com");
cookie_manager_service()->SetListAccountsResponseWebLoginRequired();
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_OK, reconcilor->GetState());
reconcilor->StartReconcile();
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_RUNNING, reconcilor->GetState());
base::RunLoop().RunUntilIdle();
std::vector<gaia::ListedAccount> accounts;
std::vector<gaia::ListedAccount> signed_out_accounts;
ASSERT_FALSE(cookie_manager_service()->ListAccounts(
&accounts, &signed_out_accounts, GaiaConstants::kChromeSource));
ASSERT_EQ(0u, accounts.size());
ASSERT_EQ(0u, signed_out_accounts.size());
base::RunLoop().RunUntilIdle();
ASSERT_EQ(signin_metrics::ACCOUNT_RECONCILOR_ERROR, reconcilor->GetState());
}
TEST_F(AccountReconcilorTest, StartReconcileNoop) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
cookie_manager_service()->SetListAccountsResponseOneAccount("user@gmail.com",
"12345");
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
histogram_tester()->ExpectTotalCount(
"Signin.Reconciler.DifferentPrimaryAccounts.FirstRun", 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.DifferentPrimaryAccounts.FirstRun",
signin_metrics::ACCOUNTS_SAME, 1);
}
TEST_F(AccountReconcilorTest, StartReconcileCookiesDisabled) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
token_service()->UpdateCredentials(account_id, "refresh_token");
test_signin_client()->set_are_signin_cookies_allowed(false);
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
reconcilor->StartReconcile();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
std::vector<gaia::ListedAccount> accounts;
// This will be the first call to ListAccounts.
ASSERT_FALSE(cookie_manager_service()->ListAccounts(
&accounts, nullptr, GaiaConstants::kChromeSource));
ASSERT_FALSE(reconcilor->is_reconcile_started_);
}
TEST_F(AccountReconcilorTest, StartReconcileContentSettings) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
token_service()->UpdateCredentials(account_id, "refresh_token");
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
test_signin_client()->set_are_signin_cookies_allowed(false);
SimulateCookieContentSettingsChanged(reconcilor,
ContentSettingsPattern::Wildcard());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
test_signin_client()->set_are_signin_cookies_allowed(true);
SimulateCookieContentSettingsChanged(reconcilor,
ContentSettingsPattern::Wildcard());
ASSERT_TRUE(reconcilor->is_reconcile_started_);
}
TEST_F(AccountReconcilorTest, StartReconcileContentSettingsGaiaUrl) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
token_service()->UpdateCredentials(account_id, "refresh_token");
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
SimulateCookieContentSettingsChanged(
reconcilor,
ContentSettingsPattern::FromURL(GaiaUrls::GetInstance()->gaia_url()));
ASSERT_TRUE(reconcilor->is_reconcile_started_);
}
TEST_F(AccountReconcilorTest, StartReconcileContentSettingsNonGaiaUrl) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
token_service()->UpdateCredentials(account_id, "refresh_token");
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
SimulateCookieContentSettingsChanged(
reconcilor,
ContentSettingsPattern::FromURL(GURL("http://www.example.com")));
ASSERT_FALSE(reconcilor->is_reconcile_started_);
}
TEST_F(AccountReconcilorTest, StartReconcileContentSettingsInvalidPattern) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
token_service()->UpdateCredentials(account_id, "refresh_token");
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
std::unique_ptr<ContentSettingsPattern::BuilderInterface> builder =
ContentSettingsPattern::CreateBuilder();
builder->Invalid();
SimulateCookieContentSettingsChanged(reconcilor, builder->Build());
ASSERT_TRUE(reconcilor->is_reconcile_started_);
}
// This test is needed until chrome changes to use gaia obfuscated id.
// The signin manager and token service use the gaia "email" property, which
// preserves dots in usernames and preserves case. gaia::ParseListAccountsData()
// however uses gaia "displayEmail" which does not preserve case, and then
// passes the string through gaia::CanonicalizeEmail() which removes dots. This
// tests makes sure that an email like "Dot.S@hmail.com", as seen by the
// token service, will be considered the same as "dots@gmail.com" as returned
// by gaia::ParseListAccountsData().
TEST_F(AccountReconcilorTest, StartReconcileNoopWithDots) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
if (account_tracker()->GetMigrationState() !=
AccountTrackerService::MIGRATION_NOT_STARTED) {
return;
}
const std::string account_id =
ConnectProfileToAccount("12345", "Dot.S@gmail.com");
cookie_manager_service()->SetListAccountsResponseOneAccount("dot.s@gmail.com",
"12345");
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
reconcilor->StartReconcile();
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.DifferentPrimaryAccounts.FirstRun",
signin_metrics::ACCOUNTS_SAME, 1);
}
TEST_F(AccountReconcilorTest, StartReconcileNoopMultiple) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
const std::string account_id2 =
PickAccountIdForAccount("67890", "other@gmail.com");
cookie_manager_service()->SetListAccountsResponseTwoAccounts(
"user@gmail.com", "12345", "other@gmail.com", "67890");
token_service()->UpdateCredentials(account_id2, "refresh_token");
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
reconcilor->StartReconcile();
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
histogram_tester()->ExpectTotalCount(
"Signin.Reconciler.DifferentPrimaryAccounts.FirstRun", 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.DifferentPrimaryAccounts.FirstRun",
signin_metrics::ACCOUNTS_SAME, 1);
}
TEST_F(AccountReconcilorTest, StartReconcileAddToCookie) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
token_service()->UpdateCredentials(account_id, "refresh_token");
cookie_manager_service()->SetListAccountsResponseOneAccount("user@gmail.com",
"12345");
const std::string account_id2 =
PickAccountIdForAccount("67890", "other@gmail.com");
token_service()->UpdateCredentials(account_id2, "refresh_token");
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id2));
AccountReconcilor* reconcilor = GetMockReconcilor();
reconcilor->StartReconcile();
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
SimulateAddAccountToCookieCompleted(reconcilor, account_id2,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.DifferentPrimaryAccounts.FirstRun",
signin_metrics::ACCOUNTS_SAME, 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.AddedToCookieJar.FirstRun", 1, 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.RemovedFromCookieJar.FirstRun", 0, 1);
base::HistogramTester::CountsMap expected_counts;
expected_counts["Signin.Reconciler.Duration.Success"] = 1;
EXPECT_THAT(histogram_tester()->GetTotalCountsForPrefix(
"Signin.Reconciler.Duration.Success"),
testing::ContainerEq(expected_counts));
}
TEST_F(AccountReconcilorTest, AuthErrorTriggersListAccount) {
class TestGaiaCookieObserver : public GaiaCookieManagerService::Observer {
public:
void OnGaiaAccountsInCookieUpdated(
const std::vector<gaia::ListedAccount>& accounts,
const std::vector<gaia::ListedAccount>& signed_out_accounts,
const GoogleServiceAuthError& error) override {
cookies_updated_ = true;
}
bool cookies_updated_ = false;
};
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
signin::AccountConsistencyMethod account_consistency =
signin::AccountConsistencyMethod::kDice;
SetAccountConsistency(account_consistency);
#else
signin::AccountConsistencyMethod account_consistency =
signin::AccountConsistencyMethod::kMirror;
SetAccountConsistency(account_consistency);
#endif
// Add one account to Chrome and instantiate the reconcilor.
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
token_service()->UpdateCredentials(account_id, "refresh_token");
TestGaiaCookieObserver observer;
cookie_manager_service()->AddObserver(&observer);
AccountReconcilor* reconcilor = GetMockReconcilor();
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
cookie_manager_service()->SetListAccountsResponseOneAccount("user@gmail.com",
"12345");
if (account_consistency == signin::AccountConsistencyMethod::kDice) {
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction())
.Times(1);
}
// Set an authentication error.
ASSERT_FALSE(observer.cookies_updated_);
token_service()->UpdateAuthErrorForTesting(
account_id, GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
GoogleServiceAuthError::InvalidGaiaCredentialsReason::
CREDENTIALS_REJECTED_BY_SERVER));
base::RunLoop().RunUntilIdle();
// Check that a call to ListAccount was triggered.
EXPECT_TRUE(observer.cookies_updated_);
testing::Mock::VerifyAndClearExpectations(GetMockReconcilor());
cookie_manager_service()->RemoveObserver(&observer);
}
#if !defined(OS_CHROMEOS)
// This test does not run on ChromeOS because it calls
// FakeSigninManagerForTesting::SignOut() which doesn't exist for ChromeOS.
TEST_F(AccountReconcilorTest, SignoutAfterErrorDoesNotRecordUma) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
token_service()->UpdateCredentials(account_id, "refresh_token");
cookie_manager_service()->SetListAccountsResponseOneAccount("user@gmail.com",
"12345");
const std::string account_id2 =
PickAccountIdForAccount("67890", "other@gmail.com");
token_service()->UpdateCredentials(account_id2, "refresh_token");
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id2));
AccountReconcilor* reconcilor = GetMockReconcilor();
reconcilor->StartReconcile();
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
GoogleServiceAuthError error(
GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);
SimulateAddAccountToCookieCompleted(reconcilor, account_id2, error);
ASSERT_FALSE(reconcilor->is_reconcile_started_);
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction());
signin_manager()->SignOut(signin_metrics::SIGNOUT_TEST,
signin_metrics::SignoutDelete::IGNORE_METRIC);
base::HistogramTester::CountsMap expected_counts;
expected_counts["Signin.Reconciler.Duration.Failure"] = 1;
EXPECT_THAT(histogram_tester()->GetTotalCountsForPrefix(
"Signin.Reconciler.Duration.Failure"),
testing::ContainerEq(expected_counts));
}
#endif // !defined(OS_CHROMEOS)
TEST_F(AccountReconcilorTest, StartReconcileRemoveFromCookie) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
token_service()->UpdateCredentials(account_id, "refresh_token");
cookie_manager_service()->SetListAccountsResponseTwoAccounts(
"user@gmail.com", "12345", "other@gmail.com", "67890");
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction());
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id));
AccountReconcilor* reconcilor = GetMockReconcilor();
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
SimulateAddAccountToCookieCompleted(reconcilor, account_id,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.DifferentPrimaryAccounts.FirstRun",
signin_metrics::ACCOUNTS_SAME, 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.AddedToCookieJar.FirstRun", 0, 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.RemovedFromCookieJar.FirstRun", 1, 1);
}
TEST_F(AccountReconcilorTest, StartReconcileAddToCookieTwice) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
const std::string account_id2 =
PickAccountIdForAccount("67890", "other@gmail.com");
const std::string account_id3 =
PickAccountIdForAccount("34567", "third@gmail.com");
cookie_manager_service()->SetListAccountsResponseOneAccount("user@gmail.com",
"12345");
token_service()->UpdateCredentials(account_id2, "refresh_token");
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id2));
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id3));
AccountReconcilor* reconcilor = GetMockReconcilor();
reconcilor->StartReconcile();
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
SimulateAddAccountToCookieCompleted(reconcilor, account_id2,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.DifferentPrimaryAccounts.FirstRun",
signin_metrics::ACCOUNTS_SAME, 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.AddedToCookieJar.FirstRun", 1, 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.RemovedFromCookieJar.FirstRun", 0, 1);
// Do another pass after I've added a third account to the token service
cookie_manager_service()->SetListAccountsResponseTwoAccounts(
"user@gmail.com", "12345", "other@gmail.com", "67890");
cookie_manager_service()->set_list_accounts_stale_for_testing(true);
// This will cause the reconcilor to fire.
token_service()->UpdateCredentials(account_id3, "refresh_token");
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
SimulateAddAccountToCookieCompleted(reconcilor, account_id3,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.DifferentPrimaryAccounts.FirstRun",
signin_metrics::ACCOUNTS_SAME, 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.AddedToCookieJar.FirstRun", 1, 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.RemovedFromCookieJar.FirstRun", 0, 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.DifferentPrimaryAccounts.SubsequentRun",
signin_metrics::ACCOUNTS_SAME, 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.AddedToCookieJar.SubsequentRun", 1, 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.RemovedFromCookieJar.SubsequentRun", 0, 1);
}
TEST_F(AccountReconcilorTest, StartReconcileBadPrimary) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
const std::string account_id2 =
PickAccountIdForAccount("67890", "other@gmail.com");
token_service()->UpdateCredentials(account_id2, "refresh_token");
cookie_manager_service()->SetListAccountsResponseTwoAccounts(
"other@gmail.com", "67890", "user@gmail.com", "12345");
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction());
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id));
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id2));
AccountReconcilor* reconcilor = GetMockReconcilor();
reconcilor->StartReconcile();
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
SimulateAddAccountToCookieCompleted(reconcilor, account_id2,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_TRUE(reconcilor->is_reconcile_started_);
SimulateAddAccountToCookieCompleted(reconcilor, account_id,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.DifferentPrimaryAccounts.FirstRun",
signin_metrics::COOKIE_AND_TOKEN_PRIMARIES_DIFFERENT, 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.AddedToCookieJar.FirstRun", 0, 1);
histogram_tester()->ExpectUniqueSample(
"Signin.Reconciler.RemovedFromCookieJar.FirstRun", 0, 1);
}
TEST_F(AccountReconcilorTest, StartReconcileOnlyOnce) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
cookie_manager_service()->SetListAccountsResponseOneAccount("user@gmail.com",
"12345");
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
ASSERT_FALSE(reconcilor->is_reconcile_started_);
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
}
TEST_F(AccountReconcilorTest, Lock) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
cookie_manager_service()->SetListAccountsResponseOneAccount("user@gmail.com",
"12345");
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
ASSERT_FALSE(reconcilor->is_reconcile_started_);
EXPECT_EQ(0, reconcilor->account_reconcilor_lock_count_);
class TestAccountReconcilorObserver : public AccountReconcilor::Observer {
public:
void OnStartReconcile() override { ++started_count_; }
void OnBlockReconcile() override { ++blocked_count_; }
void OnUnblockReconcile() override { ++unblocked_count_; }
int started_count_ = 0;
int blocked_count_ = 0;
int unblocked_count_ = 0;
};
TestAccountReconcilorObserver observer;
ScopedObserver<AccountReconcilor, AccountReconcilor::Observer>
scoped_observer(&observer);
scoped_observer.Add(reconcilor);
// Lock prevents reconcile from starting, as long as one instance is alive.
std::unique_ptr<AccountReconcilor::Lock> lock_1 =
std::make_unique<AccountReconcilor::Lock>(reconcilor);
EXPECT_EQ(1, reconcilor->account_reconcilor_lock_count_);
reconcilor->StartReconcile();
// lock_1 is blocking the reconcile.
EXPECT_FALSE(reconcilor->is_reconcile_started_);
{
AccountReconcilor::Lock lock_2(reconcilor);
EXPECT_EQ(2, reconcilor->account_reconcilor_lock_count_);
EXPECT_FALSE(reconcilor->is_reconcile_started_);
lock_1.reset();
// lock_1 is no longer blocking, but lock_2 is still alive.
EXPECT_EQ(1, reconcilor->account_reconcilor_lock_count_);
EXPECT_FALSE(reconcilor->is_reconcile_started_);
EXPECT_EQ(0, observer.started_count_);
EXPECT_EQ(0, observer.unblocked_count_);
EXPECT_EQ(1, observer.blocked_count_);
}
// All locks are deleted, reconcile starts.
EXPECT_EQ(0, reconcilor->account_reconcilor_lock_count_);
ASSERT_TRUE(reconcilor->is_reconcile_started_);
EXPECT_EQ(1, observer.started_count_);
EXPECT_EQ(1, observer.unblocked_count_);
EXPECT_EQ(1, observer.blocked_count_);
// Lock aborts current reconcile, and restarts it later.
{
AccountReconcilor::Lock lock(reconcilor);
EXPECT_EQ(1, reconcilor->account_reconcilor_lock_count_);
EXPECT_FALSE(reconcilor->is_reconcile_started_);
}
EXPECT_EQ(0, reconcilor->account_reconcilor_lock_count_);
EXPECT_TRUE(reconcilor->is_reconcile_started_);
EXPECT_EQ(2, observer.started_count_);
EXPECT_EQ(2, observer.unblocked_count_);
EXPECT_EQ(2, observer.blocked_count_);
// Reconcile can complete successfully after being restarted.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(reconcilor->is_reconcile_started_);
}
// Checks that an "invalid" Gaia account can be refreshed in place, without
// performing a full logout.
TEST_P(AccountReconcilorMethodParamTest,
StartReconcileWithSessionInfoExpiredDefault) {
SetAccountConsistency(GetParam());
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
const std::string account_id2 =
PickAccountIdForAccount("67890", "other@gmail.com");
token_service()->UpdateCredentials(account_id2, "refresh_token");
cookie_manager_service()->SetListAccountsResponseWithParams(
{{"user@gmail.com", "12345", false /* valid */, false /* signed_out */,
true /* verified */},
{"other@gmail.com", "67890", true /* valid */, false /* signed_out */,
true /* verified */}});
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id));
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
ASSERT_FALSE(reconcilor->is_reconcile_started_);
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
SimulateAddAccountToCookieCompleted(reconcilor, account_id,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
}
TEST_F(AccountReconcilorTest, AddAccountToCookieCompletedWithBogusAccount) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
cookie_manager_service()->SetListAccountsResponseOneAccountWithParams(
{"user@gmail.com", "12345", false /* valid */, false /* signed_out */,
true /* verified */});
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id));
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
ASSERT_FALSE(reconcilor->is_reconcile_started_);
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
base::RunLoop().RunUntilIdle();
// If an unknown account id is sent, it should not upset the state.
SimulateAddAccountToCookieCompleted(reconcilor, "bogus_account_id",
GoogleServiceAuthError::AuthErrorNone());
ASSERT_TRUE(reconcilor->is_reconcile_started_);
SimulateAddAccountToCookieCompleted(reconcilor, account_id,
GoogleServiceAuthError::AuthErrorNone());
ASSERT_FALSE(reconcilor->is_reconcile_started_);
}
TEST_F(AccountReconcilorTest, NoLoopWithBadPrimary) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
// Connect profile to a primary account and then add a secondary account.
const std::string account_id1 =
ConnectProfileToAccount("12345", "user@gmail.com");
const std::string account_id2 =
PickAccountIdForAccount("67890", "other@gmail.com");
token_service()->UpdateCredentials(account_id2, "refresh_token");
EXPECT_CALL(*GetMockReconcilor(), PerformLogoutAllAccountsAction());
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id1));
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id2));
// The primary account is in auth error, so it is not in the cookie.
cookie_manager_service()->SetListAccountsResponseOneAccountWithParams(
{"other@gmail.com", "67890", false /* valid */, false /* signed_out */,
true /* verified */});
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
reconcilor->StartReconcile();
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
GoogleServiceAuthError error(
GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);
// The primary cannot be added to cookie, so it fails.
SimulateAddAccountToCookieCompleted(reconcilor, account_id1, error);
SimulateAddAccountToCookieCompleted(reconcilor, account_id2,
GoogleServiceAuthError::AuthErrorNone());
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_NE(GoogleServiceAuthError::State::NONE,
reconcilor->error_during_last_reconcile_.state());
testing::Mock::VerifyAndClearExpectations(GetMockReconcilor());
// Now that we've tried once, the token service knows that the primary
// account has an auth error.
token_service()->UpdateAuthErrorForTesting(account_id1, error);
// A second attempt to reconcile should be a noop.
reconcilor->StartReconcile();
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
testing::Mock::VerifyAndClearExpectations(GetMockReconcilor());
}
TEST_F(AccountReconcilorTest, WontMergeAccountsWithError) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
// Connect profile to a primary account and then add a secondary account.
const std::string account_id1 =
ConnectProfileToAccount("12345", "user@gmail.com");
const std::string account_id2 =
PickAccountIdForAccount("67890", "other@gmail.com");
token_service()->UpdateCredentials(account_id2, "refresh_token");
// Mark the secondary account in auth error state.
token_service()->UpdateAuthErrorForTesting(
account_id2,
GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
// The cookie starts empty.
cookie_manager_service()->SetListAccountsResponseNoAccounts();
// Since the cookie jar starts empty, the reconcilor should attempt to merge
// accounts into it. However, it should only try accounts not in auth
// error state.
EXPECT_CALL(*GetMockReconcilor(), PerformMergeAction(account_id1));
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
reconcilor->StartReconcile();
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
SimulateAddAccountToCookieCompleted(reconcilor, account_id1,
GoogleServiceAuthError::AuthErrorNone());
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(reconcilor->is_reconcile_started_);
ASSERT_EQ(GoogleServiceAuthError::State::NONE,
reconcilor->error_during_last_reconcile_.state());
}
// Test that delegate timeout is called when the delegate offers a valid
// timeout.
TEST_F(AccountReconcilorTest, DelegateTimeoutIsCalled) {
const std::string account_id =
ConnectProfileToAccount("12345", "user@gmail.com");
auto spy_delegate0 = std::make_unique<SpyReconcilorDelegate>();
SpyReconcilorDelegate* spy_delegate = spy_delegate0.get();
AccountReconcilor* reconcilor = GetMockReconcilor(std::move(spy_delegate0));
ASSERT_TRUE(reconcilor);
auto timer0 = std::make_unique<base::MockOneShotTimer>();
base::MockOneShotTimer* timer = timer0.get();
reconcilor->set_timer_for_testing(std::move(timer0));
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
ASSERT_TRUE(timer->IsRunning());
// Simulate a timeout
timer->Fire();
EXPECT_EQ(1, spy_delegate->num_reconcile_timeout_calls_);
EXPECT_EQ(0, spy_delegate->num_reconcile_finished_calls_);
EXPECT_FALSE(reconcilor->is_reconcile_started_);
}
// Test that delegate timeout is not called when the delegate does not offer a
// valid timeout.
TEST_F(AccountReconcilorTest, DelegateTimeoutIsNotCalled) {
SetAccountConsistency(signin::AccountConsistencyMethod::kMirror);
ConnectProfileToAccount("12345", "user@gmail.com");
cookie_manager_service()->SetListAccountsResponseOneAccount("user@gmail.com",
"12345");
AccountReconcilor* reconcilor = GetMockReconcilor();
ASSERT_TRUE(reconcilor);
auto timer0 = std::make_unique<base::MockOneShotTimer>();
base::MockOneShotTimer* timer = timer0.get();
reconcilor->set_timer_for_testing(std::move(timer0));
reconcilor->StartReconcile();
EXPECT_TRUE(reconcilor->is_reconcile_started_);
EXPECT_FALSE(timer->IsRunning());
}
TEST_F(AccountReconcilorTest, DelegateTimeoutIsNotCalledIfTimeoutIsNotReached) {
ConnectProfileToAccount("12345", "user@gmail.com");
cookie_manager_service()->SetListAccountsResponseOneAccount("user@gmail.com",
"12345");
auto spy_delegate0 = std::make_unique<SpyReconcilorDelegate>();
SpyReconcilorDelegate* spy_delegate = spy_delegate0.get();
AccountReconcilor* reconcilor = GetMockReconcilor(std::move(spy_delegate0));
ASSERT_TRUE(reconcilor);
auto timer0 = std::make_unique<base::MockOneShotTimer>();
base::MockOneShotTimer* timer = timer0.get();
reconcilor->set_timer_for_testing(std::move(timer0));
reconcilor->StartReconcile();
ASSERT_TRUE(reconcilor->is_reconcile_started_);
ASSERT_TRUE(timer->IsRunning());
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(timer->IsRunning());
EXPECT_EQ(0, spy_delegate->num_reconcile_timeout_calls_);
EXPECT_EQ(1, spy_delegate->num_reconcile_finished_calls_);
EXPECT_FALSE(reconcilor->is_reconcile_started_);
}