blob: e471070b67af4ebdc4d4c122da5ca5db4bf8e055 [file] [log] [blame]
// Copyright 2018 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/chromeos/oauth2_token_service_delegate.h"
#include <memory>
#include <set>
#include <string>
#include <utility>
#include "base/files/scoped_temp_dir.h"
#include "base/macros.h"
#include "base/memory/scoped_refptr.h"
#include "base/stl_util.h"
#include "chromeos/account_manager/account_manager.h"
#include "components/signin/core/browser/account_info.h"
#include "components/signin/core/browser/account_tracker_service.h"
#include "components/signin/core/browser/signin_pref_names.h"
#include "components/signin/core/browser/test_signin_client.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "google_apis/gaia/gaia_urls.h"
#include "google_apis/gaia/oauth2_access_token_consumer.h"
#include "google_apis/gaia/oauth2_token_service.h"
#include "google_apis/gaia/oauth2_token_service_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace chromeos {
namespace {
using account_manager::AccountType::ACCOUNT_TYPE_GAIA;
using account_manager::AccountType::ACCOUNT_TYPE_ACTIVE_DIRECTORY;
constexpr char kGaiaId[] = "gaia-id";
constexpr char kGaiaToken[] = "gaia-token";
constexpr char kUserEmail[] = "user@gmail.com";
class AccessTokenConsumer : public OAuth2AccessTokenConsumer {
public:
AccessTokenConsumer() = default;
~AccessTokenConsumer() override = default;
void OnGetTokenSuccess(const TokenResponse& token_response) override {
++num_access_token_fetch_success_;
}
void OnGetTokenFailure(const GoogleServiceAuthError& error) override {
++num_access_token_fetch_failure_;
}
int num_access_token_fetch_success_ = 0;
int num_access_token_fetch_failure_ = 0;
private:
DISALLOW_COPY_AND_ASSIGN(AccessTokenConsumer);
};
class TokenServiceObserver : public OAuth2TokenService::Observer {
public:
void OnStartBatchChanges() override {
EXPECT_FALSE(is_inside_batch_);
is_inside_batch_ = true;
// Start a new batch
batch_change_records_.emplace_back(std::vector<std::string>());
}
void OnEndBatchChanges() override {
EXPECT_TRUE(is_inside_batch_);
is_inside_batch_ = false;
}
void OnRefreshTokenAvailable(const std::string& account_id) override {
EXPECT_TRUE(is_inside_batch_);
account_ids_.insert(account_id);
// Record the |account_id| in the last batch.
batch_change_records_.rbegin()->emplace_back(account_id);
}
void OnRefreshTokenRevoked(const std::string& account_id) override {
EXPECT_TRUE(is_inside_batch_);
account_ids_.erase(account_id);
// Record the |account_id| in the last batch.
batch_change_records_.rbegin()->emplace_back(account_id);
}
void OnAuthErrorChanged(const std::string& account_id,
const GoogleServiceAuthError& auth_error) override {
last_err_account_id_ = account_id;
last_err_ = auth_error;
}
std::string last_err_account_id_;
GoogleServiceAuthError last_err_;
std::set<std::string> account_ids_;
bool is_inside_batch_ = false;
// Records batch changes for later verification. Each index of this vector
// represents a batch change. Each batch change is a vector of account ids for
// which |OnRefreshTokenAvailable| is called.
std::vector<std::vector<std::string>> batch_change_records_;
};
} // namespace
class CrOSOAuthDelegateTest : public testing::Test {
public:
CrOSOAuthDelegateTest() {}
~CrOSOAuthDelegateTest() override = default;
protected:
void SetUp() override {
ASSERT_TRUE(tmp_dir_.CreateUniqueTempDir());
pref_service_.registry()->RegisterListPref(
AccountTrackerService::kAccountInfoPref);
pref_service_.registry()->RegisterIntegerPref(
prefs::kAccountIdMigrationState,
AccountTrackerService::MIGRATION_NOT_STARTED);
client_ = std::make_unique<TestSigninClient>(&pref_service_);
account_manager_.Initialize(tmp_dir_.GetPath(),
client_->GetURLLoaderFactory(),
immediate_callback_runner_);
thread_bundle_.RunUntilIdle();
account_tracker_service_.Initialize(&pref_service_, base::FilePath());
account_info_ = CreateAccountInfoTestFixture(kGaiaId, kUserEmail);
account_tracker_service_.SeedAccountInfo(account_info_);
gaia_account_key_ = {account_info_.gaia, ACCOUNT_TYPE_GAIA};
delegate_ = std::make_unique<ChromeOSOAuth2TokenServiceDelegate>(
&account_tracker_service_, &account_manager_);
delegate_->LoadCredentials(
account_info_.account_id /* primary_account_id */);
}
AccountInfo CreateAccountInfoTestFixture(const std::string& gaia_id,
const std::string& email) {
AccountInfo account_info;
account_info.gaia = gaia_id;
account_info.email = email;
account_info.full_name = "name";
account_info.given_name = "name";
account_info.hosted_domain = "example.com";
account_info.locale = "en";
account_info.picture_url = "https://example.com";
account_info.is_child_account = false;
account_info.account_id = account_tracker_service_.PickAccountIdForAccount(
account_info.gaia, account_info.email);
// Cannot use |ASSERT_TRUE| due to a |void| return type in an |ASSERT_TRUE|
// branch.
EXPECT_TRUE(account_info.IsValid());
return account_info;
}
void AddSuccessfulOAuthTokenResponse() {
client_->test_url_loader_factory()->AddResponse(
GaiaUrls::GetInstance()->oauth2_token_url().spec(),
GetValidTokenResponse("token", 3600));
}
// Needed because
// |content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver| in
// |ChromeOSOAuth2TokenServiceDelegate|'s constructor CHECKs that we are
// running on the browser UI thread.
content::TestBrowserThreadBundle thread_bundle_;
base::ScopedTempDir tmp_dir_;
AccountInfo account_info_;
AccountManager::AccountKey gaia_account_key_;
AccountTrackerService account_tracker_service_;
AccountManager account_manager_;
std::unique_ptr<ChromeOSOAuth2TokenServiceDelegate> delegate_;
AccountManager::DelayNetworkCallRunner immediate_callback_runner_ =
base::BindRepeating(
[](const base::RepeatingClosure& closure) -> void { closure.Run(); });
sync_preferences::TestingPrefServiceSyncable pref_service_;
std::unique_ptr<TestSigninClient> client_;
private:
DISALLOW_COPY_AND_ASSIGN(CrOSOAuthDelegateTest);
};
TEST_F(CrOSOAuthDelegateTest, RefreshTokenIsAvailableForGaiaAccounts) {
EXPECT_EQ(OAuth2TokenServiceDelegate::LoadCredentialsState::
LOAD_CREDENTIALS_FINISHED_WITH_SUCCESS,
delegate_->load_credentials_state());
EXPECT_FALSE(delegate_->RefreshTokenIsAvailable(account_info_.account_id));
account_manager_.UpsertToken(gaia_account_key_, kGaiaToken);
EXPECT_TRUE(delegate_->RefreshTokenIsAvailable(account_info_.account_id));
}
TEST_F(CrOSOAuthDelegateTest, ObserversAreNotifiedOnAuthErrorChange) {
TokenServiceObserver observer;
auto error =
GoogleServiceAuthError(GoogleServiceAuthError::State::SERVICE_ERROR);
delegate_->AddObserver(&observer);
delegate_->UpdateAuthError(account_info_.account_id, error);
EXPECT_EQ(error, delegate_->GetAuthError(account_info_.account_id));
EXPECT_EQ(account_info_.account_id, observer.last_err_account_id_);
EXPECT_EQ(error, observer.last_err_);
delegate_->RemoveObserver(&observer);
}
TEST_F(CrOSOAuthDelegateTest, ObserversAreNotifiedOnCredentialsInsertion) {
TokenServiceObserver observer;
delegate_->AddObserver(&observer);
delegate_->UpdateCredentials(account_info_.account_id, kGaiaToken);
EXPECT_EQ(1UL, observer.account_ids_.size());
EXPECT_EQ(account_info_.account_id, *observer.account_ids_.begin());
EXPECT_EQ(account_info_.account_id, observer.last_err_account_id_);
EXPECT_EQ(GoogleServiceAuthError::AuthErrorNone(), observer.last_err_);
delegate_->RemoveObserver(&observer);
}
TEST_F(CrOSOAuthDelegateTest, ObserversAreNotifiedOnCredentialsUpdate) {
TokenServiceObserver observer;
delegate_->AddObserver(&observer);
delegate_->UpdateCredentials(account_info_.account_id, kGaiaToken);
EXPECT_EQ(1UL, observer.account_ids_.size());
EXPECT_EQ(account_info_.account_id, *observer.account_ids_.begin());
EXPECT_EQ(account_info_.account_id, observer.last_err_account_id_);
EXPECT_EQ(GoogleServiceAuthError::AuthErrorNone(), observer.last_err_);
delegate_->RemoveObserver(&observer);
}
TEST_F(CrOSOAuthDelegateTest,
ObserversAreNotNotifiedIfCredentialsAreNotUpdated) {
TokenServiceObserver observer;
delegate_->AddObserver(&observer);
delegate_->UpdateCredentials(account_info_.account_id, kGaiaToken);
observer.account_ids_.clear();
observer.last_err_account_id_ = std::string();
delegate_->UpdateCredentials(account_info_.account_id, kGaiaToken);
EXPECT_TRUE(observer.account_ids_.empty());
EXPECT_EQ(std::string(), observer.last_err_account_id_);
delegate_->RemoveObserver(&observer);
}
TEST_F(CrOSOAuthDelegateTest,
BatchChangeObserversAreNotifiedOnCredentialsUpdate) {
TokenServiceObserver observer;
delegate_->AddObserver(&observer);
delegate_->UpdateCredentials(account_info_.account_id, kGaiaToken);
EXPECT_EQ(1UL, observer.batch_change_records_.size());
EXPECT_EQ(1UL, observer.batch_change_records_[0].size());
EXPECT_EQ(account_info_.account_id, observer.batch_change_records_[0][0]);
delegate_->RemoveObserver(&observer);
}
// If observers register themselves with |OAuth2TokenServiceDelegate| before
// |AccountManager| has been initialized, they should receive all the accounts
// stored in |AccountManager| in a single batch.
TEST_F(CrOSOAuthDelegateTest, BatchChangeObserversAreNotifiedOncePerBatch) {
// Setup
AccountInfo account1 = CreateAccountInfoTestFixture(
"1" /* gaia_id */, "test1@gmail.com" /* email */);
AccountInfo account2 = CreateAccountInfoTestFixture(
"2" /* gaia_id */, "test2@gmail.com" /* email */);
account_tracker_service_.SeedAccountInfo(account1);
account_tracker_service_.SeedAccountInfo(account2);
account_manager_.UpsertToken(
AccountManager::AccountKey{account1.gaia, ACCOUNT_TYPE_GAIA}, "token1");
account_manager_.UpsertToken(
AccountManager::AccountKey{account2.gaia, ACCOUNT_TYPE_GAIA}, "token2");
thread_bundle_.RunUntilIdle();
AccountManager account_manager;
// AccountManager will not be fully initialized until
// |thread_bundle_.RunUntilIdle()| is called.
account_manager.Initialize(tmp_dir_.GetPath(), client_->GetURLLoaderFactory(),
immediate_callback_runner_);
// Register callbacks before AccountManager has been fully initialized.
auto delegate = std::make_unique<ChromeOSOAuth2TokenServiceDelegate>(
&account_tracker_service_, &account_manager);
delegate->LoadCredentials(account1.account_id /* primary_account_id */);
TokenServiceObserver observer;
delegate->AddObserver(&observer);
// Wait until AccountManager is fully initialized.
thread_bundle_.RunUntilIdle();
// Tests
// The observer should receive 3 batch change callbacks:
// First - A batch of all accounts stored in AccountManager: because of the
// delegate's invocation of |AccountManager::GetAccounts| in its constructor.
// Followed by 2 updates for the individual accounts (|account1| and
// |account2|): because of the delegate's registration as an
// |AccountManager::Observer| before |AccountManager| has been fully
// initialized.
EXPECT_EQ(3UL, observer.batch_change_records_.size());
const std::vector<std::string>& first_batch =
observer.batch_change_records_[0];
EXPECT_EQ(2UL, first_batch.size());
EXPECT_TRUE(base::ContainsValue(first_batch, account1.account_id));
EXPECT_TRUE(base::ContainsValue(first_batch, account2.account_id));
delegate->RemoveObserver(&observer);
}
TEST_F(CrOSOAuthDelegateTest, GetAccountsShouldNotReturnAdAccounts) {
EXPECT_TRUE(delegate_->GetAccounts().empty());
// Insert an Active Directory account into AccountManager.
AccountManager::AccountKey ad_account_key{"object-guid",
ACCOUNT_TYPE_ACTIVE_DIRECTORY};
account_manager_.UpsertToken(ad_account_key,
AccountManager::kActiveDirectoryDummyToken);
// OAuth delegate should not return Active Directory accounts.
EXPECT_TRUE(delegate_->GetAccounts().empty());
}
TEST_F(CrOSOAuthDelegateTest, GetAccountsReturnsGaiaAccounts) {
EXPECT_TRUE(delegate_->GetAccounts().empty());
account_manager_.UpsertToken(gaia_account_key_, kGaiaToken);
std::vector<std::string> accounts = delegate_->GetAccounts();
EXPECT_EQ(1UL, accounts.size());
EXPECT_EQ(account_info_.account_id, accounts[0]);
}
TEST_F(CrOSOAuthDelegateTest, UpdateCredentialsSucceeds) {
EXPECT_TRUE(delegate_->GetAccounts().empty());
delegate_->UpdateCredentials(account_info_.account_id, kGaiaToken);
std::vector<std::string> accounts = delegate_->GetAccounts();
EXPECT_EQ(1UL, accounts.size());
EXPECT_EQ(account_info_.account_id, accounts[0]);
}
TEST_F(CrOSOAuthDelegateTest, ObserversAreNotifiedOnAccountRemoval) {
delegate_->UpdateCredentials(account_info_.account_id, kGaiaToken);
TokenServiceObserver observer;
delegate_->AddObserver(&observer);
account_manager_.RemoveAccount(gaia_account_key_);
EXPECT_EQ(1UL, observer.batch_change_records_.size());
EXPECT_EQ(1UL, observer.batch_change_records_[0].size());
EXPECT_EQ(account_info_.account_id, observer.batch_change_records_[0][0]);
EXPECT_TRUE(observer.account_ids_.empty());
delegate_->RemoveObserver(&observer);
}
TEST_F(CrOSOAuthDelegateTest,
SigninErrorObserversAreNotifiedOnAuthErrorChange) {
auto error =
GoogleServiceAuthError(GoogleServiceAuthError::State::SERVICE_ERROR);
delegate_->UpdateAuthError(account_info_.account_id, error);
EXPECT_EQ(error, delegate_->GetAuthError(account_info_.account_id));
}
TEST_F(CrOSOAuthDelegateTest, TransientErrorsAreNotShown) {
auto transient_error = GoogleServiceAuthError(
GoogleServiceAuthError::State::SERVICE_UNAVAILABLE);
EXPECT_EQ(GoogleServiceAuthError::AuthErrorNone(),
delegate_->GetAuthError(account_info_.account_id));
delegate_->UpdateAuthError(account_info_.account_id, transient_error);
EXPECT_EQ(GoogleServiceAuthError::AuthErrorNone(),
delegate_->GetAuthError(account_info_.account_id));
}
TEST_F(CrOSOAuthDelegateTest, BackOffIsTriggerredForTransientErrors) {
delegate_->UpdateCredentials(account_info_.account_id, kGaiaToken);
auto transient_error = GoogleServiceAuthError(
GoogleServiceAuthError::State::SERVICE_UNAVAILABLE);
delegate_->UpdateAuthError(account_info_.account_id, transient_error);
// Add a dummy success response. The actual network call has not been made
// yet.
AddSuccessfulOAuthTokenResponse();
// Transient error should repeat until backoff period expires.
AccessTokenConsumer access_token_consumer;
EXPECT_EQ(0, access_token_consumer.num_access_token_fetch_success_);
EXPECT_EQ(0, access_token_consumer.num_access_token_fetch_failure_);
std::vector<std::string> scopes{"scope"};
std::unique_ptr<OAuth2AccessTokenFetcher> fetcher(
delegate_->CreateAccessTokenFetcher(account_info_.account_id,
delegate_->GetURLLoaderFactory(),
&access_token_consumer));
thread_bundle_.RunUntilIdle();
fetcher->Start("client_id", "client_secret", scopes);
thread_bundle_.RunUntilIdle();
EXPECT_EQ(0, access_token_consumer.num_access_token_fetch_success_);
EXPECT_EQ(1, access_token_consumer.num_access_token_fetch_failure_);
// Expect a positive backoff time.
EXPECT_GT(delegate_->backoff_entry_.GetTimeUntilRelease(), base::TimeDelta());
// Pretend that backoff has expired and try again.
delegate_->backoff_entry_.SetCustomReleaseTime(base::TimeTicks());
fetcher.reset(delegate_->CreateAccessTokenFetcher(
account_info_.account_id, delegate_->GetURLLoaderFactory(),
&access_token_consumer));
fetcher->Start("client_id", "client_secret", scopes);
thread_bundle_.RunUntilIdle();
EXPECT_EQ(1, access_token_consumer.num_access_token_fetch_success_);
EXPECT_EQ(1, access_token_consumer.num_access_token_fetch_failure_);
}
TEST_F(CrOSOAuthDelegateTest, BackOffIsResetOnNetworkChange) {
delegate_->UpdateCredentials(account_info_.account_id, kGaiaToken);
auto transient_error = GoogleServiceAuthError(
GoogleServiceAuthError::State::SERVICE_UNAVAILABLE);
delegate_->UpdateAuthError(account_info_.account_id, transient_error);
// Add a dummy success response. The actual network call has not been made
// yet.
AddSuccessfulOAuthTokenResponse();
// Transient error should repeat until backoff period expires.
AccessTokenConsumer access_token_consumer;
EXPECT_EQ(0, access_token_consumer.num_access_token_fetch_success_);
EXPECT_EQ(0, access_token_consumer.num_access_token_fetch_failure_);
std::vector<std::string> scopes{"scope"};
std::unique_ptr<OAuth2AccessTokenFetcher> fetcher(
delegate_->CreateAccessTokenFetcher(account_info_.account_id,
delegate_->GetURLLoaderFactory(),
&access_token_consumer));
thread_bundle_.RunUntilIdle();
fetcher->Start("client_id", "client_secret", scopes);
thread_bundle_.RunUntilIdle();
EXPECT_EQ(0, access_token_consumer.num_access_token_fetch_success_);
EXPECT_EQ(1, access_token_consumer.num_access_token_fetch_failure_);
// Expect a positive backoff time.
EXPECT_GT(delegate_->backoff_entry_.GetTimeUntilRelease(), base::TimeDelta());
// Notify of network change and ensure that request now runs.
delegate_->OnConnectionChanged(
network::mojom::ConnectionType::CONNECTION_WIFI);
fetcher.reset(delegate_->CreateAccessTokenFetcher(
account_info_.account_id, delegate_->GetURLLoaderFactory(),
&access_token_consumer));
fetcher->Start("client_id", "client_secret", scopes);
thread_bundle_.RunUntilIdle();
EXPECT_EQ(1, access_token_consumer.num_access_token_fetch_success_);
EXPECT_EQ(1, access_token_consumer.num_access_token_fetch_failure_);
}
} // namespace chromeos