summaryrefslogtreecommitdiffstats
path: root/components
diff options
context:
space:
mode:
authorfgorski <fgorski@chromium.org>2014-09-04 09:48:54 -0700
committerCommit bot <commit-bot@chromium.org>2014-09-04 16:51:54 +0000
commitc1047318d54970f8176ab98b818280895045eb6c (patch)
tree3df83918e659b5bde180ef1f2f81e300d509c2d2 /components
parent6017d60fc4bfdf6ffb56f66aff105d23cb53b992 (diff)
downloadchromium_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.gyp4
-rw-r--r--components/gcm_driver.gypi4
-rw-r--r--components/gcm_driver/gcm_account_mapper.cc327
-rw-r--r--components/gcm_driver/gcm_account_mapper.h113
-rw-r--r--components/gcm_driver/gcm_account_mapper_unittest.cc563
-rw-r--r--components/gcm_driver/gcm_client.h7
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 {