| // Copyright 2014 The Chromium Authors |
| // 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 <map> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/test/task_environment.h" |
| #include "base/time/time.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[] = "[email protected]"; |
| const char kEmail2[] = "[email protected]"; |
| |
| 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(crbug.com/40582229): 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<GCMClient::AccountTokenInfo>& expected_tokens, |
| const std::vector<GCMClient::AccountTokenInfo>& 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<GCMClient::AccountTokenInfo>& 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<GCMClient::AccountTokenInfo>& 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<GCMClient::AccountTokenInfo> accounts_; |
| bool update_accounts_called_; |
| raw_ptr<GCMConnectionObserver, DanglingUntriaged> last_connection_observer_; |
| raw_ptr<GCMConnectionObserver, DanglingUntriaged> |
| 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<GCMClient::AccountTokenInfo>& 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<GCMAccountTracker> tracker_; |
| }; |
| |
| GCMAccountTrackerTest::GCMAccountTrackerTest() { |
| std::unique_ptr<AccountTracker> gaia_account_tracker( |
| new AccountTracker(identity_test_env_.identity_manager())); |
| |
| tracker_ = std::make_unique<GCMAccountTracker>( |
| 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. |
| // TODO(crbug.com/40067875): Delete account-tracking code, latest when |
| // ConsentLevel::kSync is cleaned up from the codebase. |
| 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<GCMClient::AccountTokenInfo> 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<GCMClient::AccountTokenInfo> 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<GCMClient::AccountTokenInfo> 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<GCMClient::AccountTokenInfo> expected_accounts; |
| expected_accounts.push_back(MakeAccountToken(account1)); |
| VerifyAccountTokens(expected_accounts, driver()->accounts()); |
| } |
| |
| #if !BUILDFLAG(IS_CHROMEOS) |
| // 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) |
| |
| 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<GCMClient::AccountTokenInfo> 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<GCMClient::AccountTokenInfo> 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<GCMClient::AccountTokenInfo> 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 |