// Copyright 2014 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 "components/gcm_driver/gcm_account_tracker.h" #include #include #include #include #include "base/memory/raw_ptr.h" #include "base/test/task_environment.h" #include "base/time/time.h" #include "build/chromeos_buildflags.h" #include "components/gcm_driver/fake_gcm_driver.h" #include "components/signin/public/identity_manager/identity_test_environment.h" #include "google_apis/gaia/google_service_auth_error.h" #include "net/base/ip_endpoint.h" #include "net/http/http_status_code.h" #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" #include "services/network/test/test_url_loader_factory.h" #include "services/network/test/test_utils.h" #include "testing/gtest/include/gtest/gtest.h" namespace gcm { namespace { const char kEmail1[] = "account_1@me.com"; const char kEmail2[] = "account_2@me.com"; std::string MakeAccessToken(const CoreAccountId& account_id) { return "access_token-" + account_id.ToString(); } GCMClient::AccountTokenInfo MakeAccountToken(const CoreAccountInfo& account) { GCMClient::AccountTokenInfo token_info; token_info.account_id = account.account_id; // TODO(https://crbug.com/856170): This *should* be expected to be the email // address for the given account, but there is a bug in AccountTracker that // means that |token_info.email| actually gets populated with the account ID // by the production code. Hence the test expectation has to match what the // production code actually does :). If/when that bug gets fixed, this // function should be changed to take in the email address as well as the // account ID and populate this field with the email address. token_info.email = account.email; token_info.access_token = MakeAccessToken(account.account_id); return token_info; } void VerifyAccountTokens( const std::vector& expected_tokens, const std::vector& actual_tokens) { EXPECT_EQ(expected_tokens.size(), actual_tokens.size()); for (auto expected_iter = expected_tokens.begin(), actual_iter = actual_tokens.begin(); expected_iter != expected_tokens.end() && actual_iter != actual_tokens.end(); ++expected_iter, ++actual_iter) { EXPECT_EQ(expected_iter->account_id, actual_iter->account_id); EXPECT_EQ(expected_iter->email, actual_iter->email); EXPECT_EQ(expected_iter->access_token, actual_iter->access_token); } } // This version of FakeGCMDriver is customized around handling accounts and // connection events for testing GCMAccountTracker. class CustomFakeGCMDriver : public FakeGCMDriver { public: CustomFakeGCMDriver(); CustomFakeGCMDriver(const CustomFakeGCMDriver&) = delete; CustomFakeGCMDriver& operator=(const CustomFakeGCMDriver&) = delete; ~CustomFakeGCMDriver() override; // GCMDriver overrides: void SetAccountTokens( const std::vector& account_tokens) override; void AddConnectionObserver(GCMConnectionObserver* observer) override; void RemoveConnectionObserver(GCMConnectionObserver* observer) override; bool IsConnected() const override { return connected_; } base::Time GetLastTokenFetchTime() override; void SetLastTokenFetchTime(const base::Time& time) override; // Test results and helpers. void SetConnected(bool connected); void ResetResults(); bool update_accounts_called() const { return update_accounts_called_; } const std::vector& accounts() const { return accounts_; } const GCMConnectionObserver* last_connection_observer() const { return last_connection_observer_; } const GCMConnectionObserver* last_removed_connection_observer() const { return removed_connection_observer_; } private: bool connected_; std::vector accounts_; bool update_accounts_called_; raw_ptr last_connection_observer_; raw_ptr removed_connection_observer_; net::IPEndPoint ip_endpoint_; base::Time last_token_fetch_time_; }; CustomFakeGCMDriver::CustomFakeGCMDriver() : connected_(true), update_accounts_called_(false), last_connection_observer_(nullptr), removed_connection_observer_(nullptr) {} CustomFakeGCMDriver::~CustomFakeGCMDriver() { } void CustomFakeGCMDriver::SetAccountTokens( const std::vector& accounts) { update_accounts_called_ = true; accounts_ = accounts; } void CustomFakeGCMDriver::AddConnectionObserver( GCMConnectionObserver* observer) { last_connection_observer_ = observer; } void CustomFakeGCMDriver::RemoveConnectionObserver( GCMConnectionObserver* observer) { removed_connection_observer_ = observer; } void CustomFakeGCMDriver::SetConnected(bool connected) { connected_ = connected; if (connected && last_connection_observer_) last_connection_observer_->OnConnected(ip_endpoint_); } void CustomFakeGCMDriver::ResetResults() { accounts_.clear(); update_accounts_called_ = false; last_connection_observer_ = nullptr; removed_connection_observer_ = nullptr; } base::Time CustomFakeGCMDriver::GetLastTokenFetchTime() { return last_token_fetch_time_; } void CustomFakeGCMDriver::SetLastTokenFetchTime(const base::Time& time) { last_token_fetch_time_ = time; } } // namespace class GCMAccountTrackerTest : public testing::Test { public: GCMAccountTrackerTest(); ~GCMAccountTrackerTest() override; // Helpers to pass fake info to the tracker. CoreAccountInfo AddAccount(const std::string& email); CoreAccountInfo SetPrimaryAccount(const std::string& email); void ClearPrimaryAccount(); void RemoveAccount(const CoreAccountId& account_id); // Helpers for dealing with OAuth2 access token requests. void IssueAccessToken(const CoreAccountId& account_id); void IssueExpiredAccessToken(const CoreAccountId& account_id); void IssueError(const CoreAccountId& account_id); // Accessors to account tracker and gcm driver. GCMAccountTracker* tracker() { return tracker_.get(); } CustomFakeGCMDriver* driver() { return &driver_; } // Accessors to private methods of account tracker. bool IsFetchingRequired() const; bool IsTokenReportingRequired() const; base::TimeDelta GetTimeToNextTokenReporting() const; network::TestURLLoaderFactory* test_url_loader_factory() { return &test_url_loader_factory_; } private: CustomFakeGCMDriver driver_; base::test::SingleThreadTaskEnvironment task_environment_; network::TestURLLoaderFactory test_url_loader_factory_; signin::IdentityTestEnvironment identity_test_env_; std::unique_ptr tracker_; }; GCMAccountTrackerTest::GCMAccountTrackerTest() { std::unique_ptr gaia_account_tracker( new AccountTracker(identity_test_env_.identity_manager())); tracker_ = std::make_unique( std::move(gaia_account_tracker), identity_test_env_.identity_manager(), &driver_); } GCMAccountTrackerTest::~GCMAccountTrackerTest() { if (tracker_) tracker_->Shutdown(); } CoreAccountInfo GCMAccountTrackerTest::AddAccount(const std::string& email) { return identity_test_env_.MakeAccountAvailable(email); } CoreAccountInfo GCMAccountTrackerTest::SetPrimaryAccount( const std::string& email) { // NOTE: Setting of the primary account info must be done first on ChromeOS // to ensure that AccountTracker and GCMAccountTracker respond as expected // when the token is added to the token service. // TODO(blundell): On non-ChromeOS, it would be good to add tests wherein // setting of the primary account is done afterward to check that the flow // that ensues from the GoogleSigninSucceeded callback firing works as // expected. return identity_test_env_.MakePrimaryAccountAvailable( email, signin::ConsentLevel::kSync); } void GCMAccountTrackerTest::ClearPrimaryAccount() { identity_test_env_.ClearPrimaryAccount(); } void GCMAccountTrackerTest::RemoveAccount(const CoreAccountId& account_id) { identity_test_env_.RemoveRefreshTokenForAccount(account_id); } void GCMAccountTrackerTest::IssueAccessToken(const CoreAccountId& account_id) { identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken( account_id, MakeAccessToken(account_id), base::Time::Max()); } void GCMAccountTrackerTest::IssueExpiredAccessToken( const CoreAccountId& account_id) { identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken( account_id, MakeAccessToken(account_id), base::Time::Now()); } void GCMAccountTrackerTest::IssueError(const CoreAccountId& account_id) { identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithError( account_id, GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE)); } bool GCMAccountTrackerTest::IsFetchingRequired() const { return tracker_->IsTokenFetchingRequired(); } bool GCMAccountTrackerTest::IsTokenReportingRequired() const { return tracker_->IsTokenReportingRequired(); } base::TimeDelta GCMAccountTrackerTest::GetTimeToNextTokenReporting() const { return tracker_->GetTimeToNextTokenReporting(); } TEST_F(GCMAccountTrackerTest, NoAccounts) { EXPECT_FALSE(driver()->update_accounts_called()); tracker()->Start(); // Callback should not be called if there where no accounts provided. EXPECT_FALSE(driver()->update_accounts_called()); EXPECT_TRUE(driver()->accounts().empty()); } // Verifies that callback is called after a token is issued for a single account // with a specific scope. In this scenario, the underlying account tracker is // still working when the CompleteCollectingTokens is called for the first time. TEST_F(GCMAccountTrackerTest, SingleAccount) { CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); tracker()->Start(); EXPECT_FALSE(driver()->update_accounts_called()); IssueAccessToken(account1.account_id); EXPECT_TRUE(driver()->update_accounts_called()); std::vector expected_accounts; expected_accounts.push_back(MakeAccountToken(account1)); VerifyAccountTokens(expected_accounts, driver()->accounts()); } TEST_F(GCMAccountTrackerTest, MultipleAccounts) { CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); CoreAccountInfo account2 = AddAccount(kEmail2); tracker()->Start(); EXPECT_FALSE(driver()->update_accounts_called()); IssueAccessToken(account1.account_id); EXPECT_FALSE(driver()->update_accounts_called()); IssueAccessToken(account2.account_id); EXPECT_TRUE(driver()->update_accounts_called()); std::vector expected_accounts; expected_accounts.push_back(MakeAccountToken(account1)); expected_accounts.push_back(MakeAccountToken(account2)); VerifyAccountTokens(expected_accounts, driver()->accounts()); } TEST_F(GCMAccountTrackerTest, AccountAdded) { tracker()->Start(); driver()->ResetResults(); CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); EXPECT_FALSE(driver()->update_accounts_called()); IssueAccessToken(account1.account_id); EXPECT_TRUE(driver()->update_accounts_called()); std::vector expected_accounts; expected_accounts.push_back(MakeAccountToken(account1)); VerifyAccountTokens(expected_accounts, driver()->accounts()); } TEST_F(GCMAccountTrackerTest, AccountRemoved) { CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); CoreAccountInfo account2 = AddAccount(kEmail2); tracker()->Start(); IssueAccessToken(account1.account_id); IssueAccessToken(account2.account_id); EXPECT_TRUE(driver()->update_accounts_called()); driver()->ResetResults(); EXPECT_FALSE(driver()->update_accounts_called()); RemoveAccount(account2.account_id); EXPECT_TRUE(driver()->update_accounts_called()); std::vector expected_accounts; expected_accounts.push_back(MakeAccountToken(account1)); VerifyAccountTokens(expected_accounts, driver()->accounts()); } #if !BUILDFLAG(IS_CHROMEOS_ASH) // Tests that clearing the primary account when having multiple accounts // does not crash the application. // Regression test for crbug.com/1234406 TEST_F(GCMAccountTrackerTest, AccountRemovedWithoutSyncConsentNoCrash) { CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); CoreAccountInfo account2 = AddAccount(kEmail2); // Set last fetch time to now so that access token fetch is not required // but not started. driver()->SetLastTokenFetchTime(base::Time::Now()); tracker()->Start(); EXPECT_FALSE(driver()->update_accounts_called()); // Reset the last fetch time to verify that clearing the primary account // will not trigger a token fetch. driver()->SetLastTokenFetchTime(base::Time()); EXPECT_EQ(base::TimeDelta(), GetTimeToNextTokenReporting()); ClearPrimaryAccount(); EXPECT_TRUE(driver()->update_accounts_called()); } #endif // !BUILDFLAG(IS_CHROMEOS_ASH) TEST_F(GCMAccountTrackerTest, GetTokenFailed) { CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); CoreAccountInfo account2 = AddAccount(kEmail2); tracker()->Start(); IssueAccessToken(account1.account_id); EXPECT_FALSE(driver()->update_accounts_called()); IssueError(account2.account_id); // Failed token is not retried any more. Account marked as removed. EXPECT_EQ(0UL, tracker()->get_pending_token_request_count()); EXPECT_TRUE(driver()->update_accounts_called()); std::vector expected_accounts; expected_accounts.push_back(MakeAccountToken(account1)); VerifyAccountTokens(expected_accounts, driver()->accounts()); } TEST_F(GCMAccountTrackerTest, GetTokenFailedAccountRemoved) { CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); CoreAccountInfo account2 = AddAccount(kEmail2); tracker()->Start(); IssueAccessToken(account1.account_id); driver()->ResetResults(); RemoveAccount(account2.account_id); IssueError(account2.account_id); EXPECT_TRUE(driver()->update_accounts_called()); std::vector expected_accounts; expected_accounts.push_back(MakeAccountToken(account1)); VerifyAccountTokens(expected_accounts, driver()->accounts()); } TEST_F(GCMAccountTrackerTest, AccountRemovedWhileRequestsPending) { CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); CoreAccountInfo account2 = AddAccount(kEmail2); tracker()->Start(); IssueAccessToken(account1.account_id); EXPECT_FALSE(driver()->update_accounts_called()); RemoveAccount(account2.account_id); IssueAccessToken(account2.account_id); EXPECT_TRUE(driver()->update_accounts_called()); std::vector expected_accounts; expected_accounts.push_back(MakeAccountToken(account1)); VerifyAccountTokens(expected_accounts, driver()->accounts()); } // Makes sure that tracker observes GCM connection when running. TEST_F(GCMAccountTrackerTest, TrackerObservesConnection) { EXPECT_EQ(nullptr, driver()->last_connection_observer()); tracker()->Start(); EXPECT_EQ(tracker(), driver()->last_connection_observer()); tracker()->Shutdown(); EXPECT_EQ(tracker(), driver()->last_removed_connection_observer()); } // Makes sure that token fetching happens only after connection is established. TEST_F(GCMAccountTrackerTest, PostponeTokenFetchingUntilConnected) { driver()->SetConnected(false); CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); tracker()->Start(); EXPECT_EQ(0UL, tracker()->get_pending_token_request_count()); driver()->SetConnected(true); EXPECT_EQ(1UL, tracker()->get_pending_token_request_count()); } TEST_F(GCMAccountTrackerTest, InvalidateExpiredTokens) { CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); CoreAccountInfo account2 = AddAccount(kEmail2); tracker()->Start(); EXPECT_EQ(2UL, tracker()->get_pending_token_request_count()); IssueExpiredAccessToken(account1.account_id); IssueAccessToken(account2.account_id); // Because the first token is expired, we expect the sanitize to kick in and // clean it up before the SetAccessToken is called. This also means a new // token request will be issued EXPECT_FALSE(driver()->update_accounts_called()); EXPECT_EQ(1UL, tracker()->get_pending_token_request_count()); } // Testing for whether there are still more tokens to be fetched. Typically the // need for token fetching triggers immediate request, unless there is no // connection, that is why connection is set on and off in this test. TEST_F(GCMAccountTrackerTest, IsTokenFetchingRequired) { tracker()->Start(); driver()->SetConnected(false); EXPECT_FALSE(IsFetchingRequired()); CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); EXPECT_TRUE(IsFetchingRequired()); driver()->SetConnected(true); EXPECT_FALSE(IsFetchingRequired()); // Indicates that fetching has started. IssueAccessToken(account1.account_id); EXPECT_FALSE(IsFetchingRequired()); CoreAccountInfo account2 = AddAccount(kEmail2); EXPECT_FALSE(IsFetchingRequired()); // Indicates that fetching has started. // Disconnect the driver again so that the access token request being // fulfilled doesn't immediately cause another access token request (which // then would cause IsFetchingRequired() to be false, preventing us from // distinguishing this case from the case where IsFetchingRequired() is false // because GCMAccountTracker didn't detect that a new access token needs to be // fetched). driver()->SetConnected(false); IssueExpiredAccessToken(account2.account_id); // Make sure that if the token was expired it is marked as being needed again. EXPECT_TRUE(IsFetchingRequired()); } // Tests what is the expected time to the next token fetching. TEST_F(GCMAccountTrackerTest, GetTimeToNextTokenReporting) { tracker()->Start(); // At this point the last token fetch time is never. EXPECT_EQ(base::TimeDelta(), GetTimeToNextTokenReporting()); // Regular case. The tokens have been just reported. driver()->SetLastTokenFetchTime(base::Time::Now()); EXPECT_TRUE(GetTimeToNextTokenReporting() <= base::Seconds(12 * 60 * 60)); // A case when gcm driver is not yet initialized. driver()->SetLastTokenFetchTime(base::Time::Max()); EXPECT_EQ(base::Seconds(12 * 60 * 60), GetTimeToNextTokenReporting()); // A case when token reporting calculation is expected to result in more than // 12 hours, in which case we expect exactly 12 hours. driver()->SetLastTokenFetchTime(base::Time::Now() + base::Days(2)); EXPECT_EQ(base::Seconds(12 * 60 * 60), GetTimeToNextTokenReporting()); } // Tests conditions when token reporting is required. TEST_F(GCMAccountTrackerTest, IsTokenReportingRequired) { tracker()->Start(); // Required because it is overdue. EXPECT_TRUE(IsTokenReportingRequired()); // Not required because it just happened. driver()->SetLastTokenFetchTime(base::Time::Now()); EXPECT_FALSE(IsTokenReportingRequired()); CoreAccountInfo account1 = SetPrimaryAccount(kEmail1); IssueAccessToken(account1.account_id); driver()->ResetResults(); // Reporting was triggered, which means testing for required will give false, // but we have the update call. RemoveAccount(account1.account_id); EXPECT_TRUE(driver()->update_accounts_called()); EXPECT_FALSE(IsTokenReportingRequired()); } // TODO(fgorski): Add test for adding account after removal >> make sure it does // not mark removal. } // namespace gcm