diff options
author | fgorski <fgorski@chromium.org> | 2014-09-04 09:48:54 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2014-09-04 16:51:54 +0000 |
commit | c1047318d54970f8176ab98b818280895045eb6c (patch) | |
tree | 3df83918e659b5bde180ef1f2f81e300d509c2d2 /components | |
parent | 6017d60fc4bfdf6ffb56f66aff105d23cb53b992 (diff) | |
download | chromium_src-c1047318d54970f8176ab98b818280895045eb6c.zip chromium_src-c1047318d54970f8176ab98b818280895045eb6c.tar.gz chromium_src-c1047318d54970f8176ab98b818280895045eb6c.tar.bz2 |
[GCM] Adding GCMAccountMapper to link signed in profile to accounts.
* Addes GCMAccountMapper with tests for adding and removing accoounts.
BUG=374969
Review URL: https://codereview.chromium.org/491443004
Cr-Commit-Position: refs/heads/master@{#293308}
Diffstat (limited to 'components')
-rw-r--r-- | components/components_tests.gyp | 4 | ||||
-rw-r--r-- | components/gcm_driver.gypi | 4 | ||||
-rw-r--r-- | components/gcm_driver/gcm_account_mapper.cc | 327 | ||||
-rw-r--r-- | components/gcm_driver/gcm_account_mapper.h | 113 | ||||
-rw-r--r-- | components/gcm_driver/gcm_account_mapper_unittest.cc | 563 | ||||
-rw-r--r-- | components/gcm_driver/gcm_client.h | 7 |
6 files changed, 1017 insertions, 1 deletions
diff --git a/components/components_tests.gyp b/components/components_tests.gyp index 37f3fc8..f1e0663 100644 --- a/components/components_tests.gyp +++ b/components/components_tests.gyp @@ -119,6 +119,7 @@ 'feedback/feedback_common_unittest.cc', 'feedback/feedback_data_unittest.cc', 'feedback/feedback_uploader_unittest.cc', + 'gcm_driver/gcm_account_mapper_unittest.cc', 'gcm_driver/gcm_client_impl_unittest.cc', 'gcm_driver/gcm_driver_desktop_unittest.cc', 'gcm_driver/gcm_stats_recorder_impl_unittest.cc', @@ -439,7 +440,7 @@ 'components.gyp:autofill_content_browser', 'components.gyp:autofill_content_renderer', 'components.gyp:autofill_content_test_support', - + # Dependencies of component_updater 'components.gyp:component_updater', 'components.gyp:component_updater_test_support', @@ -577,6 +578,7 @@ }], ['OS == "android"', { 'sources!': [ + 'gcm_driver/gcm_account_mapper_unittest.cc', 'gcm_driver/gcm_client_impl_unittest.cc', 'gcm_driver/gcm_driver_desktop_unittest.cc', 'feedback/feedback_common_unittest.cc', diff --git a/components/gcm_driver.gypi b/components/gcm_driver.gypi index b9336e5..7efe3c0 100644 --- a/components/gcm_driver.gypi +++ b/components/gcm_driver.gypi @@ -23,6 +23,8 @@ 'gcm_driver/android/component_jni_registrar.h', 'gcm_driver/default_gcm_app_handler.cc', 'gcm_driver/default_gcm_app_handler.h', + 'gcm_driver/gcm_account_mapper.cc', + 'gcm_driver/gcm_account_mapper.h', 'gcm_driver/gcm_activity.cc', 'gcm_driver/gcm_activity.h', 'gcm_driver/gcm_app_handler.cc', @@ -55,6 +57,8 @@ '../google_apis/gcm/gcm.gyp:gcm', ], 'sources!': [ + 'gcm_driver/gcm_account_mapper.cc', + 'gcm_driver/gcm_account_mapper.h', 'gcm_driver/gcm_client_factory.cc', 'gcm_driver/gcm_client_factory.h', 'gcm_driver/gcm_client_impl.cc', diff --git a/components/gcm_driver/gcm_account_mapper.cc b/components/gcm_driver/gcm_account_mapper.cc new file mode 100644 index 0000000..0e54fd5 --- /dev/null +++ b/components/gcm_driver/gcm_account_mapper.cc @@ -0,0 +1,327 @@ +// 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_mapper.h" + +#include "base/bind.h" +#include "base/guid.h" +#include "base/time/clock.h" +#include "base/time/default_clock.h" +#include "components/gcm_driver/gcm_driver_desktop.h" +#include "google_apis/gcm/engine/gcm_store.h" + +namespace gcm { + +namespace { + +const char kGCMAccountMapperSenderId[] = "745476177629"; +const char kGCMAccountMapperAppId[] = "com.google.android.gms"; +const int kGCMAddMappingMessageTTL = 30 * 60; // 0.5 hours in seconds. +const int kGCMRemoveMappingMessageTTL = 24 * 60 * 60; // 1 day in seconds. +const int kGCMUpdateIntervalHours = 24; +// Because adding an account mapping dependents on a fresh OAuth2 token, we +// allow the update to happen earlier than update due time, if it is within +// the early start time to take advantage of that token. +const int kGCMUpdateEarlyStartHours = 6; +const char kRegistrationIdMessgaeKey[] = "id"; +const char kTokenMessageKey[] = "t"; +const char kAccountMessageKey[] = "a"; +const char kRemoveAccountKey[] = "r"; +const char kRemoveAccountValue[] = "1"; + +std::string GenerateMessageID() { + return base::GenerateGUID(); +} + +} // namespace + +GCMAccountMapper::GCMAccountMapper(GCMDriver* gcm_driver) + : gcm_driver_(gcm_driver), + clock_(new base::DefaultClock), + initialized_(false), + weak_ptr_factory_(this) { +} + +GCMAccountMapper::~GCMAccountMapper() { +} + +void GCMAccountMapper::Initialize( + const std::vector<AccountMapping>& account_mappings, + const std::string& registration_id) { + DCHECK(!initialized_); + initialized_ = true; + registration_id_ = registration_id; + + accounts_ = account_mappings; + + gcm_driver_->AddAppHandler(kGCMAccountMapperAppId, this); + + // TODO(fgorski): if no registration ID, get registration ID. +} + +void GCMAccountMapper::SetAccountTokens( + const std::vector<GCMClient::AccountTokenInfo> account_tokens) { + DCHECK(initialized_); + + // Start from removing the old tokens, from all of the known accounts. + for (AccountMappings::iterator iter = accounts_.begin(); + iter != accounts_.end(); + ++iter) { + iter->access_token.clear(); + } + + // Update the internal collection of mappings with the new tokens. + for (std::vector<GCMClient::AccountTokenInfo>::const_iterator token_iter = + account_tokens.begin(); + token_iter != account_tokens.end(); + ++token_iter) { + AccountMapping* account_mapping = + FindMappingByAccountId(token_iter->account_id); + if (!account_mapping) { + AccountMapping new_mapping; + new_mapping.status = AccountMapping::NEW; + new_mapping.account_id = token_iter->account_id; + new_mapping.access_token = token_iter->access_token; + new_mapping.email = token_iter->email; + accounts_.push_back(new_mapping); + } else { + // Since we got a token for an account, drop the remove message and treat + // it as mapped. + if (account_mapping->status == AccountMapping::REMOVING) { + account_mapping->status = AccountMapping::MAPPED; + account_mapping->status_change_timestamp = base::Time(); + account_mapping->last_message_id.clear(); + } + + account_mapping->email = token_iter->email; + account_mapping->access_token = token_iter->access_token; + } + } + + // Decide what to do with each account (either start mapping, or start + // removing). + for (AccountMappings::iterator mappings_iter = accounts_.begin(); + mappings_iter != accounts_.end(); + ++mappings_iter) { + if (mappings_iter->access_token.empty()) { + // Send a remove message if the account was not previously being removed, + // or it doesn't have a pending message, or the pending message is + // already expired, but OnSendError event was lost. + if (mappings_iter->status != AccountMapping::REMOVING || + mappings_iter->last_message_id.empty() || + IsLastStatusChangeOlderThanTTL(*mappings_iter)) { + SendRemoveMappingMessage(*mappings_iter); + } + } else { + // A message is sent for all of the mappings considered NEW, or mappings + // that are ADDING, but have expired message (OnSendError event lost), or + // for those mapped accounts that can be refreshed. + if (mappings_iter->status == AccountMapping::NEW || + (mappings_iter->status == AccountMapping::ADDING && + IsLastStatusChangeOlderThanTTL(*mappings_iter)) || + (mappings_iter->status == AccountMapping::MAPPED && + CanTriggerUpdate(mappings_iter->status_change_timestamp))) { + mappings_iter->last_message_id.clear(); + SendAddMappingMessage(*mappings_iter); + } + } + } +} + +void GCMAccountMapper::ShutdownHandler() { + gcm_driver_->RemoveAppHandler(kGCMAccountMapperAppId); +} + +void GCMAccountMapper::OnMessage(const std::string& app_id, + const GCMClient::IncomingMessage& message) { + // Account message does not expect messages right now. +} + +void GCMAccountMapper::OnMessagesDeleted(const std::string& app_id) { + // Account message does not expect messages right now. +} + +void GCMAccountMapper::OnSendError( + const std::string& app_id, + const GCMClient::SendErrorDetails& send_error_details) { + DCHECK_EQ(app_id, kGCMAccountMapperAppId); + + AccountMappings::iterator account_mapping_it = + FindMappingByMessageId(send_error_details.message_id); + + if (account_mapping_it == accounts_.end()) + return; + + if (send_error_details.result != GCMClient::TTL_EXCEEDED) { + DVLOG(1) << "Send error result different than TTL EXCEEDED: " + << send_error_details.result << ". " + << "Postponing the retry until a new batch of tokens arrives."; + return; + } + + if (account_mapping_it->status == AccountMapping::REMOVING) { + // Another message to remove mapping can be sent immediately, because TTL + // for those is one day. No need to back off. + SendRemoveMappingMessage(*account_mapping_it); + } else { + if (account_mapping_it->status == AccountMapping::ADDING) { + // There is no mapping established, so we can remove the entry. + // Getting a fresh token will trigger a new attempt. + gcm_driver_->RemoveAccountMapping(account_mapping_it->account_id); + accounts_.erase(account_mapping_it); + } else { + // Account is already MAPPED, we have to wait for another token. + account_mapping_it->last_message_id.clear(); + gcm_driver_->UpdateAccountMapping(*account_mapping_it); + } + } +} + +void GCMAccountMapper::OnSendAcknowledged(const std::string& app_id, + const std::string& message_id) { + DCHECK_EQ(app_id, kGCMAccountMapperAppId); + AccountMappings::iterator account_mapping_it = + FindMappingByMessageId(message_id); + + DVLOG(1) << "OnSendAcknowledged with message ID: " << message_id; + + if (account_mapping_it == accounts_.end()) + return; + + // Here is where we advance a status of a mapping and persist or remove. + if (account_mapping_it->status == AccountMapping::REMOVING) { + // Message removing the account has been confirmed by the GCM, we can remove + // all the information related to the account (from memory and store). + gcm_driver_->RemoveAccountMapping(account_mapping_it->account_id); + accounts_.erase(account_mapping_it); + } else { + // Mapping status is ADDING only when it is a first time mapping. + DCHECK(account_mapping_it->status == AccountMapping::ADDING || + account_mapping_it->status == AccountMapping::MAPPED); + + // Account is marked as mapped with the current time. + account_mapping_it->status = AccountMapping::MAPPED; + account_mapping_it->status_change_timestamp = clock_->Now(); + // There is no pending message for the account. + account_mapping_it->last_message_id.clear(); + + gcm_driver_->UpdateAccountMapping(*account_mapping_it); + } +} + +bool GCMAccountMapper::CanHandle(const std::string& app_id) const { + return app_id.compare(kGCMAccountMapperAppId) == 0; +} + +void GCMAccountMapper::SendAddMappingMessage(AccountMapping& account_mapping) { + CreateAndSendMessage(account_mapping); +} + +void GCMAccountMapper::SendRemoveMappingMessage( + AccountMapping& account_mapping) { + // We want to persist an account that is being removed as quickly as possible + // as well as clean up the last message information. + if (account_mapping.status != AccountMapping::REMOVING) { + account_mapping.status = AccountMapping::REMOVING; + account_mapping.status_change_timestamp = clock_->Now(); + } + + account_mapping.last_message_id.clear(); + + gcm_driver_->UpdateAccountMapping(account_mapping); + + CreateAndSendMessage(account_mapping); +} + +void GCMAccountMapper::CreateAndSendMessage( + const AccountMapping& account_mapping) { + GCMClient::OutgoingMessage outgoing_message; + outgoing_message.id = GenerateMessageID(); + outgoing_message.data[kRegistrationIdMessgaeKey] = registration_id_; + outgoing_message.data[kAccountMessageKey] = account_mapping.email; + + if (account_mapping.status == AccountMapping::REMOVING) { + outgoing_message.time_to_live = kGCMRemoveMappingMessageTTL; + outgoing_message.data[kRemoveAccountKey] = kRemoveAccountValue; + } else { + outgoing_message.data[kTokenMessageKey] = account_mapping.access_token; + outgoing_message.time_to_live = kGCMAddMappingMessageTTL; + } + + gcm_driver_->Send(kGCMAccountMapperAppId, + kGCMAccountMapperSenderId, + outgoing_message, + base::Bind(&GCMAccountMapper::OnSendFinished, + weak_ptr_factory_.GetWeakPtr(), + account_mapping.account_id)); +} + + +void GCMAccountMapper::OnSendFinished(const std::string& account_id, + const std::string& message_id, + GCMClient::Result result) { + // TODO(fgorski): Add another attempt, in case the QUEUE is not full. + if (result != GCMClient::SUCCESS) + return; + + AccountMapping* account_mapping = FindMappingByAccountId(account_id); + DCHECK(account_mapping); + + // If we are dealing with account with status NEW, it is the first time + // mapping, and we should mark it as ADDING. + if (account_mapping->status == AccountMapping::NEW) { + account_mapping->status = AccountMapping::ADDING; + account_mapping->status_change_timestamp = clock_->Now(); + } + + account_mapping->last_message_id = message_id; + + gcm_driver_->UpdateAccountMapping(*account_mapping); +} + +bool GCMAccountMapper::CanTriggerUpdate( + const base::Time& last_update_time) const { + return last_update_time + + base::TimeDelta::FromHours(kGCMUpdateIntervalHours - + kGCMUpdateEarlyStartHours) < + clock_->Now(); +} + +bool GCMAccountMapper::IsLastStatusChangeOlderThanTTL( + const AccountMapping& account_mapping) const { + int ttl_seconds = account_mapping.status == AccountMapping::REMOVING ? + kGCMRemoveMappingMessageTTL : kGCMAddMappingMessageTTL; + return account_mapping.status_change_timestamp + + base::TimeDelta::FromSeconds(ttl_seconds) < clock_->Now(); +} + +AccountMapping* GCMAccountMapper::FindMappingByAccountId( + const std::string& account_id) { + for (AccountMappings::iterator iter = accounts_.begin(); + iter != accounts_.end(); + ++iter) { + if (iter->account_id == account_id) + return &*iter; + } + + return NULL; +} + +GCMAccountMapper::AccountMappings::iterator +GCMAccountMapper::FindMappingByMessageId(const std::string& message_id) { + for (std::vector<AccountMapping>::iterator iter = accounts_.begin(); + iter != accounts_.end(); + ++iter) { + if (iter->last_message_id == message_id) + return iter; + } + + return accounts_.end(); +} + +void GCMAccountMapper::SetClockForTesting(scoped_ptr<base::Clock> clock) { + clock_ = clock.Pass(); +} + +} // namespace gcm diff --git a/components/gcm_driver/gcm_account_mapper.h b/components/gcm_driver/gcm_account_mapper.h new file mode 100644 index 0000000..43d3365 --- /dev/null +++ b/components/gcm_driver/gcm_account_mapper.h @@ -0,0 +1,113 @@ +// 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. + +#ifndef COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_MAPPER_H_ +#define COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_MAPPER_H_ + +#include <string> +#include <vector> + +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "components/gcm_driver/gcm_app_handler.h" +#include "components/gcm_driver/gcm_client.h" +#include "google_apis/gcm/engine/account_mapping.h" + +namespace base { +class Clock; +} + +namespace gcm { + +class GCMDriver; + +// Class for mapping signed-in GAIA accounts to the GCM Device ID. +class GCMAccountMapper : public GCMAppHandler { + public: + // List of account mappings. + typedef std::vector<AccountMapping> AccountMappings; + + explicit GCMAccountMapper(GCMDriver* gcm_driver); + virtual ~GCMAccountMapper(); + + void Initialize(const AccountMappings& account_mappings, + const std::string& registration_id); + + // Called by AccountTracker, when a new list of account tokens is available. + // This will cause a refresh of account mappings and sending updates to GCM. + void SetAccountTokens( + const std::vector<GCMClient::AccountTokenInfo> account_tokens); + + // Implementation of GCMAppHandler: + virtual void ShutdownHandler() OVERRIDE; + virtual void OnMessage(const std::string& app_id, + const GCMClient::IncomingMessage& message) OVERRIDE; + virtual void OnMessagesDeleted(const std::string& app_id) OVERRIDE; + virtual void OnSendError( + const std::string& app_id, + const GCMClient::SendErrorDetails& send_error_details) OVERRIDE; + virtual void OnSendAcknowledged(const std::string& app_id, + const std::string& message_id) OVERRIDE; + virtual bool CanHandle(const std::string& app_id) const OVERRIDE; + + private: + friend class GCMAccountMapperTest; + + typedef std::map<std::string, GCMClient::OutgoingMessage> OutgoingMessages; + + // Informs GCM of an added or refreshed account mapping. + void SendAddMappingMessage(AccountMapping& account_mapping); + + // Informs GCM of a removed account mapping. + void SendRemoveMappingMessage(AccountMapping& account_mapping); + + void CreateAndSendMessage(const AccountMapping& account_mapping); + + // Callback for sending a message. + void OnSendFinished(const std::string& account_id, + const std::string& message_id, + GCMClient::Result result); + + // Checks whether the update can be triggered now. If the current time is + // within reasonable time (6 hours) of when the update is due, we want to + // trigger the update immediately to take advantage of a fresh OAuth2 token. + bool CanTriggerUpdate(const base::Time& last_update_time) const; + + // Checks whether last status change is older than a TTL of a message. + bool IsLastStatusChangeOlderThanTTL( + const AccountMapping& account_mapping) const; + + // Finds an account mapping in |accounts_| by |account_id|. + AccountMapping* FindMappingByAccountId(const std::string& account_id); + // Finds an account mapping in |accounts_| by |message_id|. + // Returns iterator that can be used to delete the account. + AccountMappings::iterator FindMappingByMessageId( + const std::string& message_id); + + // Sets the clock for testing. + void SetClockForTesting(scoped_ptr<base::Clock> clock); + + // GCMDriver owns GCMAccountMapper. + GCMDriver* gcm_driver_; + + // Clock for timestamping status changes. + scoped_ptr<base::Clock> clock_; + + // Currnetly tracked account mappings. + AccountMappings accounts_; + + // GCM Registration ID of the account mapper. + std::string registration_id_; + + bool initialized_; + + base::WeakPtrFactory<GCMAccountMapper> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(GCMAccountMapper); +}; + +} // namespace gcm + +#endif // COMPONENTS_GCM_DRIVER_GCM_ACCOUNT_MAPPER_H_ diff --git a/components/gcm_driver/gcm_account_mapper_unittest.cc b/components/gcm_driver/gcm_account_mapper_unittest.cc new file mode 100644 index 0000000..b1983e6 --- /dev/null +++ b/components/gcm_driver/gcm_account_mapper_unittest.cc @@ -0,0 +1,563 @@ +// 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_mapper.h" + +#include "base/test/simple_test_clock.h" +#include "base/time/time.h" +#include "components/gcm_driver/fake_gcm_driver.h" +#include "google_apis/gcm/engine/account_mapping.h" +#include "google_apis/gcm/engine/gcm_store.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace gcm { + +namespace { + +const char kGCMAccountMapperSenderId[] = "745476177629"; +const char kGCMAccountMapperAppId[] = "com.google.android.gms"; +const char kRegistrationId[] = "reg_id"; + +AccountMapping MakeAccountMapping(const std::string& account_id, + AccountMapping::MappingStatus status, + const base::Time& status_change_timestamp, + const std::string& last_message_id) { + AccountMapping account_mapping; + account_mapping.account_id = account_id; + account_mapping.email = account_id + "@gmail.com"; + // account_mapping.access_token intentionally left empty. + account_mapping.status = status; + account_mapping.status_change_timestamp = status_change_timestamp; + account_mapping.last_message_id = last_message_id; + return account_mapping; +} + +GCMClient::AccountTokenInfo MakeAccountTokenInfo( + const std::string& account_id) { + GCMClient::AccountTokenInfo account_token; + account_token.account_id = account_id; + account_token.email = account_id + "@gmail.com"; + account_token.access_token = account_id + "_token"; + return account_token; +} + +class CustomFakeGCMDriver : public FakeGCMDriver { + public: + CustomFakeGCMDriver(); + virtual ~CustomFakeGCMDriver(); + + virtual void UpdateAccountMapping( + const AccountMapping& account_mapping) OVERRIDE; + virtual void RemoveAccountMapping(const std::string& account_id) OVERRIDE; + virtual void AddAppHandler(const std::string& app_id, + GCMAppHandler* handler) OVERRIDE; + virtual void RemoveAppHandler(const std::string& app_id) OVERRIDE; + + void CompleteSend(const std::string& message_id, GCMClient::Result result); + void SendAcknowledged(const std::string& message_id); + void MessageSendError(const std::string& message_id); + + const AccountMapping& last_account_mapping() const { + return account_mapping_; + } + const std::string& last_message_id() const { return last_message_id_; } + const std::string& last_removed_account_id() const { + return last_removed_account_id_; + } + + protected: + virtual void SendImpl(const std::string& app_id, + const std::string& receiver_id, + const GCMClient::OutgoingMessage& message) OVERRIDE; + + private: + AccountMapping account_mapping_; + std::string last_message_id_; + std::string last_removed_account_id_; +}; + +CustomFakeGCMDriver::CustomFakeGCMDriver() { +} + +CustomFakeGCMDriver::~CustomFakeGCMDriver() { +} + +void CustomFakeGCMDriver::UpdateAccountMapping( + const AccountMapping& account_mapping) { + account_mapping_.email = account_mapping.email; + account_mapping_.account_id = account_mapping.account_id; + account_mapping_.access_token = account_mapping.access_token; + account_mapping_.status = account_mapping.status; + account_mapping_.status_change_timestamp = + account_mapping.status_change_timestamp; + account_mapping_.last_message_id = account_mapping.last_message_id; +} + +void CustomFakeGCMDriver::RemoveAccountMapping(const std::string& account_id) { + last_removed_account_id_ = account_id; +} + +void CustomFakeGCMDriver::AddAppHandler(const std::string& app_id, + GCMAppHandler* handler) { + GCMDriver::AddAppHandler(app_id, handler); +} + +void CustomFakeGCMDriver::RemoveAppHandler(const std::string& app_id) { + GCMDriver::RemoveAppHandler(app_id); +} + +void CustomFakeGCMDriver::CompleteSend(const std::string& message_id, + GCMClient::Result result) { + SendFinished(kGCMAccountMapperAppId, message_id, result); +} + +void CustomFakeGCMDriver::SendAcknowledged(const std::string& message_id) { + GetAppHandler(kGCMAccountMapperAppId) + ->OnSendAcknowledged(kGCMAccountMapperAppId, message_id); +} + +void CustomFakeGCMDriver::MessageSendError(const std::string& message_id) { + GCMClient::SendErrorDetails send_error; + send_error.message_id = message_id; + send_error.result = GCMClient::TTL_EXCEEDED; + GetAppHandler(kGCMAccountMapperAppId) + ->OnSendError(kGCMAccountMapperAppId, send_error); +} + +void CustomFakeGCMDriver::SendImpl(const std::string& app_id, + const std::string& receiver_id, + const GCMClient::OutgoingMessage& message) { + DCHECK_EQ(kGCMAccountMapperAppId, app_id); + DCHECK_EQ(kGCMAccountMapperSenderId, receiver_id); + + last_message_id_ = message.id; +} + +} // namespace + +class GCMAccountMapperTest : public testing::Test { + public: + GCMAccountMapperTest(); + virtual ~GCMAccountMapperTest(); + + void Restart(); + + const std::vector<AccountMapping>& GetAccounts() const { + return account_mapper_->accounts_; + } + + GCMAccountMapper* mapper() { return account_mapper_.get(); } + + CustomFakeGCMDriver& gcm_driver() { return gcm_driver_; } + + base::SimpleTestClock* clock() { return clock_; } + + private: + CustomFakeGCMDriver gcm_driver_; + scoped_ptr<GCMAccountMapper> account_mapper_; + base::SimpleTestClock* clock_; +}; + +GCMAccountMapperTest::GCMAccountMapperTest() { + Restart(); +} + +GCMAccountMapperTest::~GCMAccountMapperTest() { +} + +void GCMAccountMapperTest::Restart() { + if (account_mapper_) + account_mapper_->ShutdownHandler(); + account_mapper_.reset(new GCMAccountMapper(&gcm_driver_)); + scoped_ptr<base::SimpleTestClock> clock(new base::SimpleTestClock); + clock_ = clock.get(); + account_mapper_->SetClockForTesting(clock.PassAs<base::Clock>()); +} + +// Tests the initialization of account mappings (from the store) when empty. +TEST_F(GCMAccountMapperTest, InitializeAccountMappingsEmpty) { + std::vector<AccountMapping> account_mappings; + mapper()->Initialize(account_mappings, ""); + EXPECT_TRUE(GetAccounts().empty()); +} + +// Tests the initialization of account mappings (from the store). +TEST_F(GCMAccountMapperTest, InitializeAccountMappings) { + std::vector<AccountMapping> account_mappings; + AccountMapping account_mapping1 = MakeAccountMapping("acc_id1", + AccountMapping::MAPPED, + base::Time::Now(), + std::string()); + AccountMapping account_mapping2 = MakeAccountMapping("acc_id2", + AccountMapping::ADDING, + base::Time::Now(), + "add_message_1"); + account_mappings.push_back(account_mapping1); + account_mappings.push_back(account_mapping2); + + mapper()->Initialize(account_mappings, ""); + + std::vector<AccountMapping> mappings = GetAccounts(); + EXPECT_EQ(2UL, mappings.size()); + std::vector<AccountMapping>::const_iterator iter = mappings.begin(); + + EXPECT_EQ(account_mapping1.account_id, iter->account_id); + EXPECT_EQ(account_mapping1.email, iter->email); + EXPECT_TRUE(account_mapping1.access_token.empty()); + EXPECT_EQ(account_mapping1.status, iter->status); + EXPECT_EQ(account_mapping1.status_change_timestamp, + iter->status_change_timestamp); + EXPECT_TRUE(account_mapping1.last_message_id.empty()); + + ++iter; + EXPECT_EQ(account_mapping2.account_id, iter->account_id); + EXPECT_EQ(account_mapping2.email, iter->email); + EXPECT_TRUE(account_mapping2.access_token.empty()); + EXPECT_EQ(account_mapping2.status, iter->status); + EXPECT_EQ(account_mapping2.status_change_timestamp, + iter->status_change_timestamp); + EXPECT_EQ(account_mapping2.last_message_id, iter->last_message_id); +} + +// Tests the part where a new account is added with a token, to the point when +// GCM message is sent. +TEST_F(GCMAccountMapperTest, AddMappingToMessageSent) { + mapper()->Initialize(GCMAccountMapper::AccountMappings(), kRegistrationId); + + std::vector<GCMClient::AccountTokenInfo> account_tokens; + GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo("acc_id"); + account_tokens.push_back(account_token); + mapper()->SetAccountTokens(account_tokens); + + std::vector<AccountMapping> mappings = GetAccounts(); + EXPECT_EQ(1UL, mappings.size()); + std::vector<AccountMapping>::const_iterator iter = mappings.begin(); + EXPECT_EQ("acc_id", iter->account_id); + EXPECT_EQ("acc_id@gmail.com", iter->email); + EXPECT_EQ("acc_id_token", iter->access_token); + EXPECT_EQ(AccountMapping::NEW, iter->status); + EXPECT_EQ(base::Time(), iter->status_change_timestamp); + + EXPECT_TRUE(!gcm_driver().last_message_id().empty()); +} + +// Tests the part where GCM message is successfully queued. +TEST_F(GCMAccountMapperTest, AddMappingMessageQueued) { + mapper()->Initialize(GCMAccountMapper::AccountMappings(), kRegistrationId); + + std::vector<GCMClient::AccountTokenInfo> account_tokens; + GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo("acc_id"); + account_tokens.push_back(account_token); + mapper()->SetAccountTokens(account_tokens); + + clock()->SetNow(base::Time::Now()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + + EXPECT_EQ(account_token.email, gcm_driver().last_account_mapping().email); + EXPECT_EQ(account_token.account_id, + gcm_driver().last_account_mapping().account_id); + EXPECT_EQ(account_token.access_token, + gcm_driver().last_account_mapping().access_token); + EXPECT_EQ(AccountMapping::ADDING, gcm_driver().last_account_mapping().status); + EXPECT_EQ(clock()->Now(), + gcm_driver().last_account_mapping().status_change_timestamp); + EXPECT_EQ(gcm_driver().last_message_id(), + gcm_driver().last_account_mapping().last_message_id); + + std::vector<AccountMapping> mappings = GetAccounts(); + std::vector<AccountMapping>::const_iterator iter = mappings.begin(); + EXPECT_EQ(account_token.email, iter->email); + EXPECT_EQ(account_token.account_id, iter->account_id); + EXPECT_EQ(account_token.access_token, iter->access_token); + EXPECT_EQ(AccountMapping::ADDING, iter->status); + EXPECT_EQ(clock()->Now(), iter->status_change_timestamp); + EXPECT_EQ(gcm_driver().last_message_id(), iter->last_message_id); +} + +// Tests status change from ADDING to MAPPED (Message is acknowledged). +TEST_F(GCMAccountMapperTest, AddMappingMessageAcknowledged) { + mapper()->Initialize(GCMAccountMapper::AccountMappings(), kRegistrationId); + + std::vector<GCMClient::AccountTokenInfo> account_tokens; + GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo("acc_id"); + account_tokens.push_back(account_token); + mapper()->SetAccountTokens(account_tokens); + + clock()->SetNow(base::Time::Now()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + clock()->SetNow(base::Time::Now()); + gcm_driver().SendAcknowledged(gcm_driver().last_message_id()); + + EXPECT_EQ(account_token.email, gcm_driver().last_account_mapping().email); + EXPECT_EQ(account_token.account_id, + gcm_driver().last_account_mapping().account_id); + EXPECT_EQ(account_token.access_token, + gcm_driver().last_account_mapping().access_token); + EXPECT_EQ(AccountMapping::MAPPED, gcm_driver().last_account_mapping().status); + EXPECT_EQ(clock()->Now(), + gcm_driver().last_account_mapping().status_change_timestamp); + EXPECT_TRUE(gcm_driver().last_account_mapping().last_message_id.empty()); + + std::vector<AccountMapping> mappings = GetAccounts(); + std::vector<AccountMapping>::const_iterator iter = mappings.begin(); + EXPECT_EQ(account_token.email, iter->email); + EXPECT_EQ(account_token.account_id, iter->account_id); + EXPECT_EQ(account_token.access_token, iter->access_token); + EXPECT_EQ(AccountMapping::MAPPED, iter->status); + EXPECT_EQ(clock()->Now(), iter->status_change_timestamp); + EXPECT_TRUE(iter->last_message_id.empty()); +} + +// Tests status change form ADDING to MAPPED (When message was acknowledged, +// after Chrome was restarted). +TEST_F(GCMAccountMapperTest, AddMappingMessageAckedAfterRestart) { + mapper()->Initialize(GCMAccountMapper::AccountMappings(), kRegistrationId); + + std::vector<GCMClient::AccountTokenInfo> account_tokens; + GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo("acc_id"); + account_tokens.push_back(account_token); + mapper()->SetAccountTokens(account_tokens); + + clock()->SetNow(base::Time::Now()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + + Restart(); + std::vector<AccountMapping> stored_mappings; + stored_mappings.push_back(gcm_driver().last_account_mapping()); + mapper()->Initialize(stored_mappings, kRegistrationId); + + clock()->SetNow(base::Time::Now()); + gcm_driver().SendAcknowledged(gcm_driver().last_message_id()); + + EXPECT_EQ(account_token.email, gcm_driver().last_account_mapping().email); + EXPECT_EQ(account_token.account_id, + gcm_driver().last_account_mapping().account_id); + EXPECT_EQ(account_token.access_token, + gcm_driver().last_account_mapping().access_token); + EXPECT_EQ(AccountMapping::MAPPED, gcm_driver().last_account_mapping().status); + EXPECT_EQ(clock()->Now(), + gcm_driver().last_account_mapping().status_change_timestamp); + EXPECT_TRUE(gcm_driver().last_account_mapping().last_message_id.empty()); + + std::vector<AccountMapping> mappings = GetAccounts(); + std::vector<AccountMapping>::const_iterator iter = mappings.begin(); + EXPECT_EQ(account_token.email, iter->email); + EXPECT_EQ(account_token.account_id, iter->account_id); + EXPECT_EQ(account_token.access_token, iter->access_token); + EXPECT_EQ(AccountMapping::MAPPED, iter->status); + EXPECT_EQ(clock()->Now(), iter->status_change_timestamp); + EXPECT_TRUE(iter->last_message_id.empty()); +} + +// Tests a case when ADD message times out for a new account. +TEST_F(GCMAccountMapperTest, AddMappingMessageSendErrorForNewAccount) { + mapper()->Initialize(GCMAccountMapper::AccountMappings(), kRegistrationId); + + std::vector<GCMClient::AccountTokenInfo> account_tokens; + GCMClient::AccountTokenInfo account_token = MakeAccountTokenInfo("acc_id"); + account_tokens.push_back(account_token); + mapper()->SetAccountTokens(account_tokens); + + clock()->SetNow(base::Time::Now()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + + clock()->SetNow(base::Time::Now()); + std::string old_message_id = gcm_driver().last_message_id(); + gcm_driver().MessageSendError(old_message_id); + + // No new message is sent because of the send error, as the token is stale. + // Because the account was new, the entry should be deleted. + EXPECT_EQ(old_message_id, gcm_driver().last_message_id()); + EXPECT_EQ(account_token.account_id, gcm_driver().last_removed_account_id()); + EXPECT_TRUE(GetAccounts().empty()); +} + +/// Tests a case when ADD message times out for a MAPPED account. +TEST_F(GCMAccountMapperTest, AddMappingMessageSendErrorForMappedAccount) { + // Start with one account that is mapped. + base::Time status_change_timestamp = base::Time::Now(); + AccountMapping mapping = MakeAccountMapping("acc_id", + AccountMapping::MAPPED, + status_change_timestamp, + "add_message_id"); + + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(mapping); + mapper()->Initialize(stored_mappings, kRegistrationId); + + clock()->SetNow(base::Time::Now()); + gcm_driver().MessageSendError("add_message_id"); + + // No new message is sent because of the send error, as the token is stale. + // Because the account was new, the entry should be deleted. + EXPECT_TRUE(gcm_driver().last_message_id().empty()); + + std::vector<AccountMapping> mappings = GetAccounts(); + std::vector<AccountMapping>::const_iterator iter = mappings.begin(); + EXPECT_EQ(mapping.email, iter->email); + EXPECT_EQ(mapping.account_id, iter->account_id); + EXPECT_EQ(mapping.access_token, iter->access_token); + EXPECT_EQ(AccountMapping::MAPPED, iter->status); + EXPECT_EQ(status_change_timestamp, iter->status_change_timestamp); + EXPECT_TRUE(iter->last_message_id.empty()); +} + +// Tests that a missing token for an account will trigger removing of that +// account. This test goes only until the message is passed to GCM. +TEST_F(GCMAccountMapperTest, RemoveMappingToMessageSent) { + // Start with one account that is mapped. + AccountMapping mapping = MakeAccountMapping("acc_id", + AccountMapping::MAPPED, + base::Time::Now(), + std::string()); + + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(mapping); + mapper()->Initialize(stored_mappings, kRegistrationId); + clock()->SetNow(base::Time::Now()); + + mapper()->SetAccountTokens(std::vector<GCMClient::AccountTokenInfo>()); + + EXPECT_EQ(mapping.account_id, gcm_driver().last_account_mapping().account_id); + EXPECT_EQ(mapping.email, gcm_driver().last_account_mapping().email); + EXPECT_EQ(AccountMapping::REMOVING, + gcm_driver().last_account_mapping().status); + EXPECT_EQ(clock()->Now(), + gcm_driver().last_account_mapping().status_change_timestamp); + EXPECT_TRUE(gcm_driver().last_account_mapping().last_message_id.empty()); + + std::vector<AccountMapping> mappings = GetAccounts(); + std::vector<AccountMapping>::const_iterator iter = mappings.begin(); + EXPECT_EQ(mapping.email, iter->email); + EXPECT_EQ(mapping.account_id, iter->account_id); + EXPECT_EQ(mapping.access_token, iter->access_token); + EXPECT_EQ(AccountMapping::REMOVING, iter->status); + EXPECT_EQ(clock()->Now(), iter->status_change_timestamp); + EXPECT_TRUE(iter->last_message_id.empty()); +} + +// Tests that a missing token for an account will trigger removing of that +// account. This test goes until the message is queued by GCM. +TEST_F(GCMAccountMapperTest, RemoveMappingMessageQueued) { + // Start with one account that is mapped. + AccountMapping mapping = MakeAccountMapping("acc_id", + AccountMapping::MAPPED, + base::Time::Now(), + std::string()); + + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(mapping); + mapper()->Initialize(stored_mappings, kRegistrationId); + clock()->SetNow(base::Time::Now()); + base::Time status_change_timestamp = clock()->Now(); + + mapper()->SetAccountTokens(std::vector<GCMClient::AccountTokenInfo>()); + clock()->SetNow(base::Time::Now()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + + EXPECT_EQ(mapping.account_id, gcm_driver().last_account_mapping().account_id); + EXPECT_EQ(mapping.email, gcm_driver().last_account_mapping().email); + EXPECT_EQ(AccountMapping::REMOVING, + gcm_driver().last_account_mapping().status); + EXPECT_EQ(status_change_timestamp, + gcm_driver().last_account_mapping().status_change_timestamp); + EXPECT_TRUE(!gcm_driver().last_account_mapping().last_message_id.empty()); + + std::vector<AccountMapping> mappings = GetAccounts(); + std::vector<AccountMapping>::const_iterator iter = mappings.begin(); + EXPECT_EQ(mapping.email, iter->email); + EXPECT_EQ(mapping.account_id, iter->account_id); + EXPECT_EQ(mapping.access_token, iter->access_token); + EXPECT_EQ(AccountMapping::REMOVING, iter->status); + EXPECT_EQ(status_change_timestamp, iter->status_change_timestamp); + EXPECT_EQ(gcm_driver().last_account_mapping().last_message_id, + iter->last_message_id); +} + +// Tests that a missing token for an account will trigger removing of that +// account. This test goes until the message is acknowledged by GCM. +// This is a complete success scenario for account removal, and it end with +// account mapping being completely gone. +TEST_F(GCMAccountMapperTest, RemoveMappingMessageAcknowledged) { + // Start with one account that is mapped. + AccountMapping mapping = MakeAccountMapping("acc_id", + AccountMapping::MAPPED, + base::Time::Now(), + std::string()); + + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(mapping); + mapper()->Initialize(stored_mappings, kRegistrationId); + clock()->SetNow(base::Time::Now()); + + mapper()->SetAccountTokens(std::vector<GCMClient::AccountTokenInfo>()); + gcm_driver().CompleteSend(gcm_driver().last_message_id(), GCMClient::SUCCESS); + gcm_driver().SendAcknowledged(gcm_driver().last_message_id()); + + EXPECT_EQ(mapping.account_id, gcm_driver().last_removed_account_id()); + + std::vector<AccountMapping> mappings = GetAccounts(); + EXPECT_TRUE(mappings.empty()); +} + +// Tests that account removing proceeds, when a removing message is acked after +// Chrome was restarted. +TEST_F(GCMAccountMapperTest, RemoveMappingMessageAckedAfterRestart) { + // Start with one account that is mapped. + AccountMapping mapping = MakeAccountMapping("acc_id", + AccountMapping::REMOVING, + base::Time::Now(), + "remove_message_id"); + + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(mapping); + mapper()->Initialize(stored_mappings, kRegistrationId); + + gcm_driver().SendAcknowledged("remove_message_id"); + + EXPECT_EQ(mapping.account_id, gcm_driver().last_removed_account_id()); + + std::vector<AccountMapping> mappings = GetAccounts(); + EXPECT_TRUE(mappings.empty()); +} + +// Tests that account removing proceeds, when a removing message is acked after +// Chrome was restarted. +TEST_F(GCMAccountMapperTest, RemoveMappingMessageSendError) { + // Start with one account that is mapped. + base::Time status_change_timestamp = base::Time::Now(); + AccountMapping mapping = MakeAccountMapping("acc_id", + AccountMapping::REMOVING, + status_change_timestamp, + "remove_message_id"); + + GCMAccountMapper::AccountMappings stored_mappings; + stored_mappings.push_back(mapping); + mapper()->Initialize(stored_mappings, kRegistrationId); + + clock()->SetNow(base::Time::Now()); + gcm_driver().MessageSendError("remove_message_id"); + + EXPECT_TRUE(gcm_driver().last_removed_account_id().empty()); + + EXPECT_EQ(mapping.account_id, gcm_driver().last_account_mapping().account_id); + EXPECT_EQ(mapping.email, gcm_driver().last_account_mapping().email); + EXPECT_EQ(AccountMapping::REMOVING, + gcm_driver().last_account_mapping().status); + EXPECT_EQ(status_change_timestamp, + gcm_driver().last_account_mapping().status_change_timestamp); + // Message is not persisted, until send is completed. + EXPECT_TRUE(gcm_driver().last_account_mapping().last_message_id.empty()); + + std::vector<AccountMapping> mappings = GetAccounts(); + std::vector<AccountMapping>::const_iterator iter = mappings.begin(); + EXPECT_EQ(mapping.email, iter->email); + EXPECT_EQ(mapping.account_id, iter->account_id); + EXPECT_TRUE(iter->access_token.empty()); + EXPECT_EQ(AccountMapping::REMOVING, iter->status); + EXPECT_EQ(status_change_timestamp, iter->status_change_timestamp); + EXPECT_TRUE(iter->last_message_id.empty()); +} + +} // namespace gcm diff --git a/components/gcm_driver/gcm_client.h b/components/gcm_driver/gcm_client.h index 8a7f7b2..825c9fd 100644 --- a/components/gcm_driver/gcm_client.h +++ b/components/gcm_driver/gcm_client.h @@ -141,6 +141,13 @@ class GCMClient { RecordedActivities recorded_activities; }; + // Information about account. + struct AccountTokenInfo { + std::string account_id; + std::string email; + std::string access_token; + }; + // A delegate interface that allows the GCMClient instance to interact with // its caller, i.e. notifying asynchronous event. class Delegate { |