// 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