// Copyright (c) 2011 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.
// Unit tests for the SyncApi. Note that a lot of the underlying
// functionality is provided by the Syncable layer, which has its own
// unit tests. We'll test SyncApi specific things in this harness.
#include <map>
#include "base/basictypes.h"
#include "base/format_macros.h"
#include "base/memory/scoped_ptr.h"
#include "base/memory/scoped_temp_dir.h"
#include "base/message_loop.h"
#include "base/string_number_conversions.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/sync/engine/http_post_provider_factory.h"
#include "chrome/browser/sync/engine/http_post_provider_interface.h"
#include "chrome/browser/sync/engine/model_safe_worker.h"
#include "chrome/browser/sync/engine/syncapi.h"
#include "chrome/browser/sync/js_arg_list.h"
#include "chrome/browser/sync/js_backend.h"
#include "chrome/browser/sync/js_event_handler.h"
#include "chrome/browser/sync/js_event_router.h"
#include "chrome/browser/sync/js_test_util.h"
#include "chrome/browser/sync/notifier/sync_notifier.h"
#include "chrome/browser/sync/notifier/sync_notifier_observer.h"
#include "chrome/browser/sync/protocol/password_specifics.pb.h"
#include "chrome/browser/sync/protocol/proto_value_conversions.h"
#include "chrome/browser/sync/sessions/sync_session.h"
#include "chrome/browser/sync/syncable/directory_manager.h"
#include "chrome/browser/sync/syncable/nigori_util.h"
#include "chrome/browser/sync/syncable/syncable.h"
#include "chrome/browser/sync/syncable/syncable_id.h"
#include "chrome/browser/sync/util/cryptographer.h"
#include "chrome/test/sync/engine/test_user_share.h"
#include "chrome/test/values_test_util.h"
#include "content/browser/browser_thread.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using browser_sync::Cryptographer;
using browser_sync::HasArgsAsList;
using browser_sync::KeyParams;
using browser_sync::JsArgList;
using browser_sync::MockJsEventHandler;
using browser_sync::MockJsEventRouter;
using browser_sync::ModelSafeRoutingInfo;
using browser_sync::ModelSafeWorker;
using browser_sync::ModelSafeWorkerRegistrar;
using browser_sync::sessions::SyncSessionSnapshot;
using syncable::ModelType;
using syncable::ModelTypeSet;
using test::ExpectDictionaryValue;
using test::ExpectStringValue;
using testing::_;
using testing::AtLeast;
using testing::Invoke;
using testing::SaveArg;
using testing::StrictMock;
namespace sync_api {
namespace {
void ExpectInt64Value(int64 expected_value,
const DictionaryValue& value, const std::string& key) {
std::string int64_str;
EXPECT_TRUE(value.GetString(key, &int64_str));
int64 val = 0;
EXPECT_TRUE(base::StringToInt64(int64_str, &val));
EXPECT_EQ(expected_value, val);
}
// Makes a non-folder child of the root node. Returns the id of the
// newly-created node.
int64 MakeNode(UserShare* share,
ModelType model_type,
const std::string& client_tag) {
WriteTransaction trans(share);
ReadNode root_node(&trans);
root_node.InitByRootLookup();
WriteNode node(&trans);
EXPECT_TRUE(node.InitUniqueByCreation(model_type, root_node, client_tag));
node.SetIsFolder(false);
return node.GetId();
}
// Make a folder as a child of the root node. Returns the id of the
// newly-created node.
int64 MakeFolder(UserShare* share,
syncable::ModelType model_type,
const std::string& client_tag) {
WriteTransaction trans(share);
ReadNode root_node(&trans);
root_node.InitByRootLookup();
WriteNode node(&trans);
EXPECT_TRUE(node.InitUniqueByCreation(model_type, root_node, client_tag));
node.SetIsFolder(true);
return node.GetId();
}
// Makes a non-folder child of a non-root node. Returns the id of the
// newly-created node.
int64 MakeNodeWithParent(UserShare* share,
ModelType model_type,
const std::string& client_tag,
int64 parent_id) {
WriteTransaction trans(share);
ReadNode parent_node(&trans);
parent_node.InitByIdLookup(parent_id);
WriteNode node(&trans);
EXPECT_TRUE(node.InitUniqueByCreation(model_type, parent_node, client_tag));
node.SetIsFolder(false);
return node.GetId();
}
// Makes a folder child of a non-root node. Returns the id of the
// newly-created node.
int64 MakeFolderWithParent(UserShare* share,
ModelType model_type,
int64 parent_id,
BaseNode* predecessor) {
WriteTransaction trans(share);
ReadNode parent_node(&trans);
parent_node.InitByIdLookup(parent_id);
WriteNode node(&trans);
EXPECT_TRUE(node.InitByCreation(model_type, parent_node, predecessor));
node.SetIsFolder(true);
return node.GetId();
}
// Creates the "synced" root node for a particular datatype. We use the syncable
// methods here so that the syncer treats these nodes as if they were already
// received from the server.
int64 MakeServerNodeForType(UserShare* share,
ModelType model_type) {
sync_pb::EntitySpecifics specifics;
syncable::AddDefaultExtensionValue(model_type, &specifics);
syncable::ScopedDirLookup dir(share->dir_manager.get(), share->name);
EXPECT_TRUE(dir.good());
syncable::WriteTransaction trans(dir, syncable::UNITTEST, __FILE__, __LINE__);
// Attempt to lookup by nigori tag.
std::string type_tag = syncable::ModelTypeToRootTag(model_type);
syncable::Id node_id = syncable::Id::CreateFromServerId(type_tag);
syncable::MutableEntry entry(&trans, syncable::CREATE_NEW_UPDATE_ITEM,
node_id);
EXPECT_TRUE(entry.good());
entry.Put(syncable::BASE_VERSION, 1);
entry.Put(syncable::SERVER_VERSION, 1);
entry.Put(syncable::IS_UNAPPLIED_UPDATE, false);
entry.Put(syncable::SERVER_PARENT_ID, syncable::kNullId);
entry.Put(syncable::SERVER_IS_DIR, true);
entry.Put(syncable::IS_DIR, true);
entry.Put(syncable::SERVER_SPECIFICS, specifics);
entry.Put(syncable::UNIQUE_SERVER_TAG, type_tag);
entry.Put(syncable::NON_UNIQUE_NAME, type_tag);
entry.Put(syncable::IS_DEL, false);
entry.Put(syncable::SPECIFICS, specifics);
return entry.Get(syncable::META_HANDLE);
}
} // namespace
class SyncApiTest : public testing::Test {
public:
virtual void SetUp() {
test_user_share_.SetUp();
}
virtual void TearDown() {
test_user_share_.TearDown();
}
protected:
browser_sync::TestUserShare test_user_share_;
};
TEST_F(SyncApiTest, SanityCheckTest) {
{
ReadTransaction trans(test_user_share_.user_share());
EXPECT_TRUE(trans.GetWrappedTrans() != NULL);
}
{
WriteTransaction trans(test_user_share_.user_share());
EXPECT_TRUE(trans.GetWrappedTrans() != NULL);
}
{
// No entries but root should exist
ReadTransaction trans(test_user_share_.user_share());
ReadNode node(&trans);
// Metahandle 1 can be root, sanity check 2
EXPECT_FALSE(node.InitByIdLookup(2));
}
}
TEST_F(SyncApiTest, BasicTagWrite) {
{
ReadTransaction trans(test_user_share_.user_share());
ReadNode root_node(&trans);
root_node.InitByRootLookup();
EXPECT_EQ(root_node.GetFirstChildId(), 0);
}
ignore_result(MakeNode(test_user_share_.user_share(),
syncable::BOOKMARKS, "testtag"));
{
ReadTransaction trans(test_user_share_.user_share());
ReadNode node(&trans);
EXPECT_TRUE(node.InitByClientTagLookup(syncable::BOOKMARKS,
"testtag"));
ReadNode root_node(&trans);
root_node.InitByRootLookup();
EXPECT_NE(node.GetId(), 0);
EXPECT_EQ(node.GetId(), root_node.GetFirstChildId());
}
}
TEST_F(SyncApiTest, GenerateSyncableHash) {
EXPECT_EQ("OyaXV5mEzrPS4wbogmtKvRfekAI=",
BaseNode::GenerateSyncableHash(syncable::BOOKMARKS, "tag1"));
EXPECT_EQ("iNFQtRFQb+IZcn1kKUJEZDDkLs4=",
BaseNode::GenerateSyncableHash(syncable::PREFERENCES, "tag1"));
EXPECT_EQ("gO1cPZQXaM73sHOvSA+tKCKFs58=",
BaseNode::GenerateSyncableHash(syncable::AUTOFILL, "tag1"));
EXPECT_EQ("A0eYIHXM1/jVwKDDp12Up20IkKY=",
BaseNode::GenerateSyncableHash(syncable::BOOKMARKS, "tag2"));
EXPECT_EQ("XYxkF7bhS4eItStFgiOIAU23swI=",
BaseNode::GenerateSyncableHash(syncable::PREFERENCES, "tag2"));
EXPECT_EQ("GFiWzo5NGhjLlN+OyCfhy28DJTQ=",
BaseNode::GenerateSyncableHash(syncable::AUTOFILL, "tag2"));
}
TEST_F(SyncApiTest, ModelTypesSiloed) {
{
WriteTransaction trans(test_user_share_.user_share());
ReadNode root_node(&trans);
root_node.InitByRootLookup();
EXPECT_EQ(root_node.GetFirstChildId(), 0);
}
ignore_result(MakeNode(test_user_share_.user_share(),
syncable::BOOKMARKS, "collideme"));
ignore_result(MakeNode(test_user_share_.user_share(),
syncable::PREFERENCES, "collideme"));
ignore_result(MakeNode(test_user_share_.user_share(),
syncable::AUTOFILL, "collideme"));
{
ReadTransaction trans(test_user_share_.user_share());
ReadNode bookmarknode(&trans);
EXPECT_TRUE(bookmarknode.InitByClientTagLookup(syncable::BOOKMARKS,
"collideme"));
ReadNode prefnode(&trans);
EXPECT_TRUE(prefnode.InitByClientTagLookup(syncable::PREFERENCES,
"collideme"));
ReadNode autofillnode(&trans);
EXPECT_TRUE(autofillnode.InitByClientTagLookup(syncable::AUTOFILL,
"collideme"));
EXPECT_NE(bookmarknode.GetId(), prefnode.GetId());
EXPECT_NE(autofillnode.GetId(), prefnode.GetId());
EXPECT_NE(bookmarknode.GetId(), autofillnode.GetId());
}
}
TEST_F(SyncApiTest, ReadMissingTagsFails) {
{
ReadTransaction trans(test_user_share_.user_share());
ReadNode node(&trans);
EXPECT_FALSE(node.InitByClientTagLookup(syncable::BOOKMARKS,
"testtag"));
}
{
WriteTransaction trans(test_user_share_.user_share());
WriteNode node(&trans);
EXPECT_FALSE(node.InitByClientTagLookup(syncable::BOOKMARKS,
"testtag"));
}
}
// TODO(chron): Hook this all up to the server and write full integration tests
// for update->undelete behavior.
TEST_F(SyncApiTest, TestDeleteBehavior) {
int64 node_id;
int64 folder_id;
std::wstring test_title(L"test1");
{
WriteTransaction trans(test_user_share_.user_share());
ReadNode root_node(&trans);
root_node.InitByRootLookup();
// we'll use this spare folder later
WriteNode folder_node(&trans);
EXPECT_TRUE(folder_node.InitByCreation(syncable::BOOKMARKS,
root_node, NULL));
folder_id = folder_node.GetId();
WriteNode wnode(&trans);
EXPECT_TRUE(wnode.InitUniqueByCreation(syncable::BOOKMARKS,
root_node, "testtag"));
wnode.SetIsFolder(false);
wnode.SetTitle(test_title);
node_id = wnode.GetId();
}
// Ensure we can delete something with a tag.
{
WriteTransaction trans(test_user_share_.user_share());
WriteNode wnode(&trans);
EXPECT_TRUE(wnode.InitByClientTagLookup(syncable::BOOKMARKS,
"testtag"));
EXPECT_FALSE(wnode.GetIsFolder());
EXPECT_EQ(wnode.GetTitle(), test_title);
wnode.Remove();
}
// Lookup of a node which was deleted should return failure,
// but have found some data about the node.
{
ReadTransaction trans(test_user_share_.user_share());
ReadNode node(&trans);
EXPECT_FALSE(node.InitByClientTagLookup(syncable::BOOKMARKS,
"testtag"));
// Note that for proper function of this API this doesn't need to be
// filled, we're checking just to make sure the DB worked in this test.
EXPECT_EQ(node.GetTitle(), test_title);
}
{
WriteTransaction trans(test_user_share_.user_share());
ReadNode folder_node(&trans);
EXPECT_TRUE(folder_node.InitByIdLookup(folder_id));
WriteNode wnode(&trans);
// This will undelete the tag.
EXPECT_TRUE(wnode.InitUniqueByCreation(syncable::BOOKMARKS,
folder_node, "testtag"));
EXPECT_EQ(wnode.GetIsFolder(), false);
EXPECT_EQ(wnode.GetParentId(), folder_node.GetId());
EXPECT_EQ(wnode.GetId(), node_id);
EXPECT_NE(wnode.GetTitle(), test_title); // Title should be cleared
wnode.SetTitle(test_title);
}
// Now look up should work.
{
ReadTransaction trans(test_user_share_.user_share());
ReadNode node(&trans);
EXPECT_TRUE(node.InitByClientTagLookup(syncable::BOOKMARKS,
"testtag"));
EXPECT_EQ(node.GetTitle(), test_title);
EXPECT_EQ(node.GetModelType(), syncable::BOOKMARKS);
}
}
TEST_F(SyncApiTest, WriteAndReadPassword) {
KeyParams params = {"localhost", "username", "passphrase"};
{
ReadTransaction trans(test_user_share_.user_share());
trans.GetCryptographer()->AddKey(params);
}
{
WriteTransaction trans(test_user_share_.user_share());
ReadNode root_node(&trans);
root_node.InitByRootLookup();
WriteNode password_node(&trans);
EXPECT_TRUE(password_node.InitUniqueByCreation(syncable::PASSWORDS,
root_node, "foo"));
sync_pb::PasswordSpecificsData data;
data.set_password_value("secret");
password_node.SetPasswordSpecifics(data);
}
{
ReadTransaction trans(test_user_share_.user_share());
ReadNode root_node(&trans);
root_node.InitByRootLookup();
ReadNode password_node(&trans);
EXPECT_TRUE(password_node.InitByClientTagLookup(syncable::PASSWORDS,
"foo"));
const sync_pb::PasswordSpecificsData& data =
password_node.GetPasswordSpecifics();
EXPECT_EQ("secret", data.password_value());
}
}
namespace {
void CheckNodeValue(const BaseNode& node, const DictionaryValue& value) {
ExpectInt64Value(node.GetId(), value, "id");
ExpectInt64Value(node.GetModificationTime(), value, "modificationTime");
ExpectInt64Value(node.GetParentId(), value, "parentId");
{
bool is_folder = false;
EXPECT_TRUE(value.GetBoolean("isFolder", &is_folder));
EXPECT_EQ(node.GetIsFolder(), is_folder);
}
ExpectStringValue(WideToUTF8(node.GetTitle()), value, "title");
{
ModelType expected_model_type = node.GetModelType();
std::string type_str;
EXPECT_TRUE(value.GetString("type", &type_str));
if (expected_model_type >= syncable::FIRST_REAL_MODEL_TYPE) {
ModelType model_type =
syncable::ModelTypeFromString(type_str);
EXPECT_EQ(expected_model_type, model_type);
} else if (expected_model_type == syncable::TOP_LEVEL_FOLDER) {
EXPECT_EQ("Top-level folder", type_str);
} else if (expected_model_type == syncable::UNSPECIFIED) {
EXPECT_EQ("Unspecified", type_str);
} else {
ADD_FAILURE();
}
}
ExpectInt64Value(node.GetExternalId(), value, "externalId");
ExpectInt64Value(node.GetPredecessorId(), value, "predecessorId");
ExpectInt64Value(node.GetSuccessorId(), value, "successorId");
ExpectInt64Value(node.GetFirstChildId(), value, "firstChildId");
{
scoped_ptr<DictionaryValue> expected_entry(node.GetEntry()->ToValue());
Value* entry = NULL;
EXPECT_TRUE(value.Get("entry", &entry));
EXPECT_TRUE(Value::Equals(entry, expected_entry.get()));
}
EXPECT_EQ(11u, value.size());
}
} // namespace
TEST_F(SyncApiTest, BaseNodeToValue) {
ReadTransaction trans(test_user_share_.user_share());
ReadNode node(&trans);
node.InitByRootLookup();
scoped_ptr<DictionaryValue> value(node.ToValue());
if (value.get()) {
CheckNodeValue(node, *value);
} else {
ADD_FAILURE();
}
}
namespace {
void ExpectChangeRecordActionValue(SyncManager::ChangeRecord::Action
expected_value,
const DictionaryValue& value,
const std::string& key) {
std::string str_value;
EXPECT_TRUE(value.GetString(key, &str_value));
switch (expected_value) {
case SyncManager::ChangeRecord::ACTION_ADD:
EXPECT_EQ("Add", str_value);
break;
case SyncManager::ChangeRecord::ACTION_UPDATE:
EXPECT_EQ("Update", str_value);
break;
case SyncManager::ChangeRecord::ACTION_DELETE:
EXPECT_EQ("Delete", str_value);
break;
default:
NOTREACHED();
break;
}
}
void CheckNonDeleteChangeRecordValue(const SyncManager::ChangeRecord& record,
const DictionaryValue& value,
BaseTransaction* trans) {
EXPECT_NE(SyncManager::ChangeRecord::ACTION_DELETE, record.action);
ExpectChangeRecordActionValue(record.action, value, "action");
{
ReadNode node(trans);
EXPECT_TRUE(node.InitByIdLookup(record.id));
scoped_ptr<DictionaryValue> expected_node_value(node.ToValue());
ExpectDictionaryValue(*expected_node_value, value, "node");
}
}
void CheckDeleteChangeRecordValue(const SyncManager::ChangeRecord& record,
const DictionaryValue& value) {
EXPECT_EQ(SyncManager::ChangeRecord::ACTION_DELETE, record.action);
ExpectChangeRecordActionValue(record.action, value, "action");
DictionaryValue* node_value = NULL;
EXPECT_TRUE(value.GetDictionary("node", &node_value));
if (node_value) {
ExpectInt64Value(record.id, *node_value, "id");
scoped_ptr<DictionaryValue> expected_specifics_value(
browser_sync::EntitySpecificsToValue(record.specifics));
ExpectDictionaryValue(*expected_specifics_value,
*node_value, "specifics");
scoped_ptr<DictionaryValue> expected_extra_value;
if (record.extra.get()) {
expected_extra_value.reset(record.extra->ToValue());
}
Value* extra_value = NULL;
EXPECT_EQ(record.extra.get() != NULL,
node_value->Get("extra", &extra_value));
EXPECT_TRUE(Value::Equals(extra_value, expected_extra_value.get()));
}
}
class MockExtraChangeRecordData
: public SyncManager::ExtraPasswordChangeRecordData {
public:
MOCK_CONST_METHOD0(ToValue, DictionaryValue*());
};
} // namespace
TEST_F(SyncApiTest, ChangeRecordToValue) {
int64 child_id = MakeNode(test_user_share_.user_share(),
syncable::BOOKMARKS, "testtag");
sync_pb::EntitySpecifics child_specifics;
{
ReadTransaction trans(test_user_share_.user_share());
ReadNode node(&trans);
EXPECT_TRUE(node.InitByIdLookup(child_id));
child_specifics = node.GetEntry()->Get(syncable::SPECIFICS);
}
// Add
{
ReadTransaction trans(test_user_share_.user_share());
SyncManager::ChangeRecord record;
record.action = SyncManager::ChangeRecord::ACTION_ADD;
record.id = 1;
record.specifics = child_specifics;
record.extra.reset(new StrictMock<MockExtraChangeRecordData>());
scoped_ptr<DictionaryValue> value(record.ToValue(&trans));
CheckNonDeleteChangeRecordValue(record, *value, &trans);
}
// Update
{
ReadTransaction trans(test_user_share_.user_share());
SyncManager::ChangeRecord record;
record.action = SyncManager::ChangeRecord::ACTION_UPDATE;
record.id = child_id;
record.specifics = child_specifics;
record.extra.reset(new StrictMock<MockExtraChangeRecordData>());
scoped_ptr<DictionaryValue> value(record.ToValue(&trans));
CheckNonDeleteChangeRecordValue(record, *value, &trans);
}
// Delete (no extra)
{
ReadTransaction trans(test_user_share_.user_share());
SyncManager::ChangeRecord record;
record.action = SyncManager::ChangeRecord::ACTION_DELETE;
record.id = child_id + 1;
record.specifics = child_specifics;
scoped_ptr<DictionaryValue> value(record.ToValue(&trans));
CheckDeleteChangeRecordValue(record, *value);
}
// Delete (with extra)
{
ReadTransaction trans(test_user_share_.user_share());
SyncManager::ChangeRecord record;
record.action = SyncManager::ChangeRecord::ACTION_DELETE;
record.id = child_id + 1;
record.specifics = child_specifics;
DictionaryValue extra_value;
extra_value.SetString("foo", "bar");
scoped_ptr<StrictMock<MockExtraChangeRecordData> > extra(
new StrictMock<MockExtraChangeRecordData>());
EXPECT_CALL(*extra, ToValue()).Times(2).WillRepeatedly(
Invoke(&extra_value, &DictionaryValue::DeepCopy));
record.extra.reset(extra.release());
scoped_ptr<DictionaryValue> value(record.ToValue(&trans));
CheckDeleteChangeRecordValue(record, *value);
}
}
namespace {
class TestHttpPostProviderFactory : public HttpPostProviderFactory {
public:
virtual ~TestHttpPostProviderFactory() {}
virtual HttpPostProviderInterface* Create() {
NOTREACHED();
return NULL;
}
virtual void Destroy(HttpPostProviderInterface* http) {
NOTREACHED();
}
};
class SyncManagerObserverMock : public SyncManager::Observer {
public:
MOCK_METHOD4(OnChangesApplied,
void(ModelType,
const BaseTransaction*,
const SyncManager::ChangeRecord*,
int)); // NOLINT
MOCK_METHOD1(OnChangesComplete, void(ModelType)); // NOLINT
MOCK_METHOD1(OnSyncCycleCompleted,
void(const SyncSessionSnapshot*)); // NOLINT
MOCK_METHOD0(OnInitializationComplete, void()); // NOLINT
MOCK_METHOD1(OnAuthError, void(const GoogleServiceAuthError&)); // NOLINT
MOCK_METHOD1(OnPassphraseRequired, void(bool)); // NOLINT
MOCK_METHOD0(OnPassphraseFailed, void()); // NOLINT
MOCK_METHOD1(OnPassphraseAccepted, void(const std::string&)); // NOLINT
MOCK_METHOD0(OnStopSyncingPermanently, void()); // NOLINT
MOCK_METHOD1(OnUpdatedToken, void(const std::string&)); // NOLINT
MOCK_METHOD1(OnMigrationNeededForTypes, void(const ModelTypeSet&));
MOCK_METHOD0(OnClearServerDataFailed, void()); // NOLINT
MOCK_METHOD0(OnClearServerDataSucceeded, void()); // NOLINT
MOCK_METHOD1(OnEncryptionComplete, void(const ModelTypeSet&)); // NOLINT
};
class SyncNotifierMock : public sync_notifier::SyncNotifier {
public:
MOCK_METHOD1(AddObserver, void(sync_notifier::SyncNotifierObserver*));
MOCK_METHOD1(RemoveObserver, void(sync_notifier::SyncNotifierObserver*));
MOCK_METHOD1(SetState, void(const std::string&));
MOCK_METHOD2(UpdateCredentials,
void(const std::string&, const std::string&));
MOCK_METHOD1(UpdateEnabledTypes,
void(const syncable::ModelTypeSet&));
MOCK_METHOD0(SendNotification, void());
};
class SyncManagerTest : public testing::Test,
public ModelSafeWorkerRegistrar {
protected:
SyncManagerTest()
: ui_thread_(BrowserThread::UI, &ui_loop_),
sync_notifier_observer_(NULL),
update_enabled_types_call_count_(0) {}
// Test implementation.
void SetUp() {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
SyncCredentials credentials;
credentials.email = "foo@bar.com";
credentials.sync_token = "sometoken";
sync_notifier_mock_.reset(new StrictMock<SyncNotifierMock>());
EXPECT_CALL(*sync_notifier_mock_, AddObserver(_)).
WillOnce(Invoke(this, &SyncManagerTest::SyncNotifierAddObserver));
EXPECT_CALL(*sync_notifier_mock_, SetState(""));
EXPECT_CALL(*sync_notifier_mock_,
UpdateCredentials(credentials.email, credentials.sync_token));
EXPECT_CALL(*sync_notifier_mock_, UpdateEnabledTypes(_)).
Times(AtLeast(1)).
WillRepeatedly(
Invoke(this, &SyncManagerTest::SyncNotifierUpdateEnabledTypes));
EXPECT_CALL(*sync_notifier_mock_, RemoveObserver(_)).
WillOnce(Invoke(this, &SyncManagerTest::SyncNotifierRemoveObserver));
EXPECT_FALSE(sync_notifier_observer_);
sync_manager_.Init(temp_dir_.path(), "bogus", 0, false,
new TestHttpPostProviderFactory(), this, "bogus",
credentials, sync_notifier_mock_.get(), "",
true /* setup_for_test_mode */);
EXPECT_TRUE(sync_notifier_observer_);
sync_manager_.AddObserver(&observer_);
EXPECT_EQ(1, update_enabled_types_call_count_);
ModelSafeRoutingInfo routes;
GetModelSafeRoutingInfo(&routes);
for (ModelSafeRoutingInfo::iterator i = routes.begin(); i != routes.end();
++i) {
EXPECT_CALL(observer_, OnChangesApplied(i->first, _, _, 1))
.RetiresOnSaturation();
EXPECT_CALL(observer_, OnChangesComplete(i->first))
.RetiresOnSaturation();
type_roots_[i->first] = MakeServerNodeForType(
sync_manager_.GetUserShare(), i->first);
}
}
void TearDown() {
sync_manager_.RemoveObserver(&observer_);
sync_manager_.Shutdown();
EXPECT_FALSE(sync_notifier_observer_);
}
// ModelSafeWorkerRegistrar implementation.
virtual void GetWorkers(std::vector<ModelSafeWorker*>* out) {
NOTIMPLEMENTED();
out->clear();
}
virtual void GetModelSafeRoutingInfo(ModelSafeRoutingInfo* out) {
(*out)[syncable::NIGORI] = browser_sync::GROUP_PASSIVE;
(*out)[syncable::BOOKMARKS] = browser_sync::GROUP_PASSIVE;
(*out)[syncable::THEMES] = browser_sync::GROUP_PASSIVE;
(*out)[syncable::SESSIONS] = browser_sync::GROUP_PASSIVE;
(*out)[syncable::PASSWORDS] = browser_sync::GROUP_PASSIVE;
}
// Helper methods.
bool SetUpEncryption() {
// We need to create the nigori node as if it were an applied server update.
UserShare* share = sync_manager_.GetUserShare();
int64 nigori_id = GetIdForDataType(syncable::NIGORI);
if (nigori_id == kInvalidId)
return false;
// Set the nigori cryptographer information.
WriteTransaction trans(share);
Cryptographer* cryptographer = trans.GetCryptographer();
if (!cryptographer)
return false;
KeyParams params = {"localhost", "dummy", "foobar"};
cryptographer->AddKey(params);
sync_pb::NigoriSpecifics nigori;
cryptographer->GetKeys(nigori.mutable_encrypted());
WriteNode node(&trans);
node.InitByIdLookup(nigori_id);
node.SetNigoriSpecifics(nigori);
return cryptographer->is_ready();
}
int64 GetIdForDataType(ModelType type) {
if (type_roots_.count(type) == 0)
return 0;
return type_roots_[type];
}
void SyncNotifierAddObserver(
sync_notifier::SyncNotifierObserver* sync_notifier_observer) {
EXPECT_EQ(NULL, sync_notifier_observer_);
sync_notifier_observer_ = sync_notifier_observer;
}
void SyncNotifierRemoveObserver(
sync_notifier::SyncNotifierObserver* sync_notifier_observer) {
EXPECT_EQ(sync_notifier_observer_, sync_notifier_observer);
sync_notifier_observer_ = NULL;
}
void SyncNotifierUpdateEnabledTypes(
const syncable::ModelTypeSet& types) {
ModelSafeRoutingInfo routes;
GetModelSafeRoutingInfo(&routes);
syncable::ModelTypeSet expected_types;
for (ModelSafeRoutingInfo::const_iterator it = routes.begin();
it != routes.end(); ++it) {
expected_types.insert(it->first);
}
EXPECT_EQ(expected_types, types);
++update_enabled_types_call_count_;
}
private:
// Needed by |ui_thread_|.
MessageLoopForUI ui_loop_;
// Needed by |sync_manager_|.
BrowserThread ui_thread_;
// Needed by |sync_manager_|.
ScopedTempDir temp_dir_;
// Sync Id's for the roots of the enabled datatypes.
std::map<ModelType, int64> type_roots_;
scoped_ptr<StrictMock<SyncNotifierMock> > sync_notifier_mock_;
protected:
SyncManager sync_manager_;
StrictMock<SyncManagerObserverMock> observer_;
sync_notifier::SyncNotifierObserver* sync_notifier_observer_;
int update_enabled_types_call_count_;
};
TEST_F(SyncManagerTest, UpdateEnabledTypes) {
EXPECT_EQ(1, update_enabled_types_call_count_);
// Triggers SyncNotifierUpdateEnabledTypes.
sync_manager_.UpdateEnabledTypes();
EXPECT_EQ(2, update_enabled_types_call_count_);
}
TEST_F(SyncManagerTest, ParentJsEventRouter) {
StrictMock<MockJsEventRouter> event_router;
browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();
EXPECT_EQ(NULL, js_backend->GetParentJsEventRouter());
js_backend->SetParentJsEventRouter(&event_router);
EXPECT_EQ(&event_router, js_backend->GetParentJsEventRouter());
js_backend->RemoveParentJsEventRouter();
EXPECT_EQ(NULL, js_backend->GetParentJsEventRouter());
}
TEST_F(SyncManagerTest, ProcessMessage) {
const JsArgList kNoArgs;
browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();
// Messages sent without any parent router should be dropped.
{
StrictMock<MockJsEventHandler> event_handler;
js_backend->ProcessMessage("unknownMessage",
kNoArgs, &event_handler);
js_backend->ProcessMessage("getNotificationState",
kNoArgs, &event_handler);
}
{
StrictMock<MockJsEventHandler> event_handler;
StrictMock<MockJsEventRouter> event_router;
ListValue false_args;
false_args.Append(Value::CreateBooleanValue(false));
EXPECT_CALL(event_router,
RouteJsEvent("onGetNotificationStateFinished",
HasArgsAsList(false_args), &event_handler));
js_backend->SetParentJsEventRouter(&event_router);
// This message should be dropped.
js_backend->ProcessMessage("unknownMessage",
kNoArgs, &event_handler);
// This should trigger the reply.
js_backend->ProcessMessage("getNotificationState",
kNoArgs, &event_handler);
js_backend->RemoveParentJsEventRouter();
}
// Messages sent after a parent router has been removed should be
// dropped.
{
StrictMock<MockJsEventHandler> event_handler;
js_backend->ProcessMessage("unknownMessage",
kNoArgs, &event_handler);
js_backend->ProcessMessage("getNotificationState",
kNoArgs, &event_handler);
}
}
TEST_F(SyncManagerTest, ProcessMessageGetRootNode) {
const JsArgList kNoArgs;
browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();
StrictMock<MockJsEventHandler> event_handler;
StrictMock<MockJsEventRouter> event_router;
JsArgList return_args;
EXPECT_CALL(event_router,
RouteJsEvent("onGetRootNodeFinished", _, &event_handler)).
WillOnce(SaveArg<1>(&return_args));
js_backend->SetParentJsEventRouter(&event_router);
// Should trigger the reply.
js_backend->ProcessMessage("getRootNode", kNoArgs, &event_handler);
EXPECT_EQ(1u, return_args.Get().GetSize());
DictionaryValue* node_info = NULL;
EXPECT_TRUE(return_args.Get().GetDictionary(0, &node_info));
if (node_info) {
ReadTransaction trans(sync_manager_.GetUserShare());
ReadNode node(&trans);
node.InitByRootLookup();
CheckNodeValue(node, *node_info);
} else {
ADD_FAILURE();
}
js_backend->RemoveParentJsEventRouter();
}
void CheckGetNodeByIdReturnArgs(const SyncManager& sync_manager,
const JsArgList& return_args,
int64 id) {
EXPECT_EQ(1u, return_args.Get().GetSize());
DictionaryValue* node_info = NULL;
EXPECT_TRUE(return_args.Get().GetDictionary(0, &node_info));
if (node_info) {
ReadTransaction trans(sync_manager.GetUserShare());
ReadNode node(&trans);
node.InitByIdLookup(id);
CheckNodeValue(node, *node_info);
} else {
ADD_FAILURE();
}
}
TEST_F(SyncManagerTest, ProcessMessageGetNodeById) {
int64 child_id =
MakeNode(sync_manager_.GetUserShare(), syncable::BOOKMARKS, "testtag");
browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();
StrictMock<MockJsEventHandler> event_handler;
StrictMock<MockJsEventRouter> event_router;
JsArgList return_args;
EXPECT_CALL(event_router,
RouteJsEvent("onGetNodeByIdFinished", _, &event_handler))
.Times(2).WillRepeatedly(SaveArg<1>(&return_args));
js_backend->SetParentJsEventRouter(&event_router);
// Should trigger the reply.
{
ListValue args;
args.Append(Value::CreateStringValue("1"));
js_backend->ProcessMessage("getNodeById", JsArgList(args), &event_handler);
}
CheckGetNodeByIdReturnArgs(sync_manager_, return_args, 1);
// Should trigger another reply.
{
ListValue args;
args.Append(Value::CreateStringValue(base::Int64ToString(child_id)));
js_backend->ProcessMessage("getNodeById", JsArgList(args), &event_handler);
}
CheckGetNodeByIdReturnArgs(sync_manager_, return_args, child_id);
js_backend->RemoveParentJsEventRouter();
}
TEST_F(SyncManagerTest, ProcessMessageGetNodeByIdFailure) {
browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();
StrictMock<MockJsEventHandler> event_handler;
StrictMock<MockJsEventRouter> event_router;
ListValue null_args;
null_args.Append(Value::CreateNullValue());
EXPECT_CALL(event_router,
RouteJsEvent("onGetNodeByIdFinished",
HasArgsAsList(null_args), &event_handler))
.Times(5);
js_backend->SetParentJsEventRouter(&event_router);
{
ListValue args;
js_backend->ProcessMessage("getNodeById", JsArgList(args), &event_handler);
}
{
ListValue args;
args.Append(Value::CreateStringValue(""));
js_backend->ProcessMessage("getNodeById", JsArgList(args), &event_handler);
}
{
ListValue args;
args.Append(Value::CreateStringValue("nonsense"));
js_backend->ProcessMessage("getNodeById", JsArgList(args), &event_handler);
}
{
ListValue args;
args.Append(Value::CreateStringValue("nonsense"));
js_backend->ProcessMessage("getNodeById", JsArgList(args), &event_handler);
}
{
ListValue args;
args.Append(Value::CreateStringValue("0"));
js_backend->ProcessMessage("getNodeById", JsArgList(args), &event_handler);
}
// TODO(akalin): Figure out how to test InitByIdLookup() failure.
js_backend->RemoveParentJsEventRouter();
}
TEST_F(SyncManagerTest, OnNotificationStateChange) {
StrictMock<MockJsEventRouter> event_router;
ListValue true_args;
true_args.Append(Value::CreateBooleanValue(true));
ListValue false_args;
false_args.Append(Value::CreateBooleanValue(false));
EXPECT_CALL(event_router,
RouteJsEvent("onSyncNotificationStateChange",
HasArgsAsList(true_args), NULL));
EXPECT_CALL(event_router,
RouteJsEvent("onSyncNotificationStateChange",
HasArgsAsList(false_args), NULL));
browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();
sync_manager_.TriggerOnNotificationStateChangeForTest(true);
sync_manager_.TriggerOnNotificationStateChangeForTest(false);
js_backend->SetParentJsEventRouter(&event_router);
sync_manager_.TriggerOnNotificationStateChangeForTest(true);
sync_manager_.TriggerOnNotificationStateChangeForTest(false);
js_backend->RemoveParentJsEventRouter();
sync_manager_.TriggerOnNotificationStateChangeForTest(true);
sync_manager_.TriggerOnNotificationStateChangeForTest(false);
}
TEST_F(SyncManagerTest, OnIncomingNotification) {
StrictMock<MockJsEventRouter> event_router;
const syncable::ModelTypeBitSet empty_model_types;
syncable::ModelTypeBitSet model_types;
model_types.set(syncable::BOOKMARKS);
model_types.set(syncable::THEMES);
// Build expected_args to have a single argument with the string
// equivalents of model_types.
ListValue expected_args;
{
ListValue* model_type_list = new ListValue();
expected_args.Append(model_type_list);
for (int i = syncable::FIRST_REAL_MODEL_TYPE;
i < syncable::MODEL_TYPE_COUNT; ++i) {
if (model_types[i]) {
model_type_list->Append(
Value::CreateStringValue(
syncable::ModelTypeToString(
syncable::ModelTypeFromInt(i))));
}
}
}
EXPECT_CALL(event_router,
RouteJsEvent("onSyncIncomingNotification",
HasArgsAsList(expected_args), NULL));
browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();
sync_manager_.TriggerOnIncomingNotificationForTest(empty_model_types);
sync_manager_.TriggerOnIncomingNotificationForTest(model_types);
js_backend->SetParentJsEventRouter(&event_router);
sync_manager_.TriggerOnIncomingNotificationForTest(model_types);
js_backend->RemoveParentJsEventRouter();
sync_manager_.TriggerOnIncomingNotificationForTest(empty_model_types);
sync_manager_.TriggerOnIncomingNotificationForTest(model_types);
}
TEST_F(SyncManagerTest, EncryptDataTypesWithNoData) {
EXPECT_TRUE(SetUpEncryption());
ModelTypeSet encrypted_types;
encrypted_types.insert(syncable::BOOKMARKS);
// Even though Passwords isn't marked for encryption, it's enabled, so it
// should automatically be added to the response of OnEncryptionComplete.
ModelTypeSet expected_types = encrypted_types;
expected_types.insert(syncable::PASSWORDS);
EXPECT_CALL(observer_, OnEncryptionComplete(expected_types));
sync_manager_.EncryptDataTypes(encrypted_types);
{
ReadTransaction trans(sync_manager_.GetUserShare());
EXPECT_EQ(encrypted_types,
GetEncryptedDataTypes(trans.GetWrappedTrans()));
}
}
TEST_F(SyncManagerTest, EncryptDataTypesWithData) {
size_t batch_size = 5;
EXPECT_TRUE(SetUpEncryption());
// Create some unencrypted unsynced data.
int64 folder = MakeFolderWithParent(sync_manager_.GetUserShare(),
syncable::BOOKMARKS,
GetIdForDataType(syncable::BOOKMARKS),
NULL);
// First batch_size nodes are children of folder.
size_t i;
for (i = 0; i < batch_size; ++i) {
MakeNodeWithParent(sync_manager_.GetUserShare(), syncable::BOOKMARKS,
StringPrintf("%"PRIuS"", i), folder);
}
// Next batch_size nodes are a different type and on their own.
for (; i < 2*batch_size; ++i) {
MakeNodeWithParent(sync_manager_.GetUserShare(), syncable::SESSIONS,
StringPrintf("%"PRIuS"", i),
GetIdForDataType(syncable::SESSIONS));
}
// Last batch_size nodes are a third type that will not need encryption.
for (; i < 3*batch_size; ++i) {
MakeNodeWithParent(sync_manager_.GetUserShare(), syncable::THEMES,
StringPrintf("%"PRIuS"", i),
GetIdForDataType(syncable::THEMES));
}
{
ReadTransaction trans(sync_manager_.GetUserShare());
EXPECT_TRUE(syncable::VerifyDataTypeEncryption(trans.GetWrappedTrans(),
syncable::BOOKMARKS,
false /* not encrypted */));
EXPECT_TRUE(syncable::VerifyDataTypeEncryption(trans.GetWrappedTrans(),
syncable::SESSIONS,
false /* not encrypted */));
EXPECT_TRUE(syncable::VerifyDataTypeEncryption(trans.GetWrappedTrans(),
syncable::THEMES,
false /* not encrypted */));
}
ModelTypeSet encrypted_types;
encrypted_types.insert(syncable::BOOKMARKS);
encrypted_types.insert(syncable::SESSIONS);
encrypted_types.insert(syncable::PASSWORDS);
EXPECT_CALL(observer_, OnEncryptionComplete(encrypted_types));
sync_manager_.EncryptDataTypes(encrypted_types);
{
ReadTransaction trans(sync_manager_.GetUserShare());
encrypted_types.erase(syncable::PASSWORDS); // Not stored in nigori node.
EXPECT_EQ(encrypted_types,
GetEncryptedDataTypes(trans.GetWrappedTrans()));
EXPECT_TRUE(syncable::VerifyDataTypeEncryption(trans.GetWrappedTrans(),
syncable::BOOKMARKS,
true /* is encrypted */));
EXPECT_TRUE(syncable::VerifyDataTypeEncryption(trans.GetWrappedTrans(),
syncable::SESSIONS,
true /* is encrypted */));
EXPECT_TRUE(syncable::VerifyDataTypeEncryption(trans.GetWrappedTrans(),
syncable::THEMES,
false /* not encrypted */));
}
}
} // namespace
} // namespace browser_sync