// Copyright (c) 2006-2009 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 "chrome/browser/sync/engine/apply_updates_command.h" #include "chrome/browser/sync/protocol/bookmark_specifics.pb.h" #include "chrome/browser/sync/sessions/sync_session.h" #include "chrome/browser/sync/syncable/directory_manager.h" #include "chrome/browser/sync/syncable/syncable.h" #include "chrome/browser/sync/syncable/syncable_id.h" #include "chrome/test/sync/engine/syncer_command_test.h" #include "testing/gtest/include/gtest/gtest.h" namespace browser_sync { using sessions::SyncSession; using std::string; using syncable::Entry; using syncable::Id; using syncable::MutableEntry; using syncable::ReadTransaction; using syncable::ScopedDirLookup; using syncable::UNITTEST; using syncable::WriteTransaction; // A test fixture for tests exercising ApplyUpdatesCommand. class ApplyUpdatesCommandTest : public SyncerCommandTest { public: protected: ApplyUpdatesCommandTest() : next_revision_(1) {} virtual ~ApplyUpdatesCommandTest() {} virtual void SetUp() { workers()->clear(); mutable_routing_info()->clear(); workers()->push_back(new ModelSafeWorker()); // GROUP_PASSIVE worker. (*mutable_routing_info())[syncable::BOOKMARKS] = GROUP_PASSIVE; (*mutable_routing_info())[syncable::PASSWORDS] = GROUP_PASSIVE; (*mutable_routing_info())[syncable::NIGORI] = GROUP_PASSIVE; SyncerCommandTest::SetUp(); } // Create a new unapplied update. void CreateUnappliedNewItemWithParent(const string& item_id, const string& parent_id) { ScopedDirLookup dir(syncdb()->manager(), syncdb()->name()); ASSERT_TRUE(dir.good()); WriteTransaction trans(dir, UNITTEST, __FILE__, __LINE__); MutableEntry entry(&trans, syncable::CREATE_NEW_UPDATE_ITEM, Id::CreateFromServerId(item_id)); ASSERT_TRUE(entry.good()); entry.Put(syncable::SERVER_VERSION, next_revision_++); entry.Put(syncable::IS_UNAPPLIED_UPDATE, true); entry.Put(syncable::SERVER_NON_UNIQUE_NAME, item_id); entry.Put(syncable::SERVER_PARENT_ID, Id::CreateFromServerId(parent_id)); entry.Put(syncable::SERVER_IS_DIR, true); sync_pb::EntitySpecifics default_bookmark_specifics; default_bookmark_specifics.MutableExtension(sync_pb::bookmark); entry.Put(syncable::SERVER_SPECIFICS, default_bookmark_specifics); } void CreateUnappliedNewItem(const string& item_id, const sync_pb::EntitySpecifics& specifics) { ScopedDirLookup dir(syncdb()->manager(), syncdb()->name()); ASSERT_TRUE(dir.good()); WriteTransaction trans(dir, UNITTEST, __FILE__, __LINE__); MutableEntry entry(&trans, syncable::CREATE_NEW_UPDATE_ITEM, Id::CreateFromServerId(item_id)); ASSERT_TRUE(entry.good()); entry.Put(syncable::SERVER_VERSION, next_revision_++); entry.Put(syncable::IS_UNAPPLIED_UPDATE, true); entry.Put(syncable::SERVER_NON_UNIQUE_NAME, item_id); entry.Put(syncable::SERVER_PARENT_ID, syncable::kNullId); entry.Put(syncable::SERVER_IS_DIR, false); entry.Put(syncable::SERVER_SPECIFICS, specifics); } ApplyUpdatesCommand apply_updates_command_; private: int64 next_revision_; DISALLOW_COPY_AND_ASSIGN(ApplyUpdatesCommandTest); }; TEST_F(ApplyUpdatesCommandTest, Simple) { string root_server_id = syncable::kNullId.GetServerId(); CreateUnappliedNewItemWithParent("parent", root_server_id); CreateUnappliedNewItemWithParent("child", "parent"); apply_updates_command_.ExecuteImpl(session()); sessions::StatusController* status = session()->status_controller(); sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); EXPECT_EQ(2, status->update_progress().AppliedUpdatesSize()) << "All updates should have been attempted"; EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize()) << "Simple update shouldn't result in conflicts"; EXPECT_EQ(2, status->update_progress().SuccessfullyAppliedUpdateCount()) << "All items should have been successfully applied"; } TEST_F(ApplyUpdatesCommandTest, UpdateWithChildrenBeforeParents) { // Set a bunch of updates which are difficult to apply in the order // they're received due to dependencies on other unseen items. string root_server_id = syncable::kNullId.GetServerId(); CreateUnappliedNewItemWithParent("a_child_created_first", "parent"); CreateUnappliedNewItemWithParent("x_child_created_first", "parent"); CreateUnappliedNewItemWithParent("parent", root_server_id); CreateUnappliedNewItemWithParent("a_child_created_second", "parent"); CreateUnappliedNewItemWithParent("x_child_created_second", "parent"); apply_updates_command_.ExecuteImpl(session()); sessions::StatusController* status = session()->status_controller(); sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); EXPECT_EQ(5, status->update_progress().AppliedUpdatesSize()) << "All updates should have been attempted"; EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize()) << "Simple update shouldn't result in conflicts, even if out-of-order"; EXPECT_EQ(5, status->update_progress().SuccessfullyAppliedUpdateCount()) << "All updates should have been successfully applied"; } TEST_F(ApplyUpdatesCommandTest, NestedItemsWithUnknownParent) { // We shouldn't be able to do anything with either of these items. CreateUnappliedNewItemWithParent("some_item", "unknown_parent"); CreateUnappliedNewItemWithParent("some_other_item", "some_item"); apply_updates_command_.ExecuteImpl(session()); sessions::StatusController* status = session()->status_controller(); sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); EXPECT_EQ(2, status->update_progress().AppliedUpdatesSize()) << "All updates should have been attempted"; EXPECT_EQ(2, status->conflict_progress().ConflictingItemsSize()) << "All updates with an unknown ancestors should be in conflict"; EXPECT_EQ(0, status->update_progress().SuccessfullyAppliedUpdateCount()) << "No item with an unknown ancestor should be applied"; } TEST_F(ApplyUpdatesCommandTest, ItemsBothKnownAndUnknown) { // See what happens when there's a mixture of good and bad updates. string root_server_id = syncable::kNullId.GetServerId(); CreateUnappliedNewItemWithParent("first_unknown_item", "unknown_parent"); CreateUnappliedNewItemWithParent("first_known_item", root_server_id); CreateUnappliedNewItemWithParent("second_unknown_item", "unknown_parent"); CreateUnappliedNewItemWithParent("second_known_item", "first_known_item"); CreateUnappliedNewItemWithParent("third_known_item", "fourth_known_item"); CreateUnappliedNewItemWithParent("fourth_known_item", root_server_id); apply_updates_command_.ExecuteImpl(session()); sessions::StatusController* status = session()->status_controller(); sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); EXPECT_EQ(6, status->update_progress().AppliedUpdatesSize()) << "All updates should have been attempted"; EXPECT_EQ(2, status->conflict_progress().ConflictingItemsSize()) << "The updates with unknown ancestors should be in conflict"; EXPECT_EQ(4, status->update_progress().SuccessfullyAppliedUpdateCount()) << "The updates with known ancestors should be successfully applied"; } TEST_F(ApplyUpdatesCommandTest, DecryptablePassword) { // Decryptable password updates should be applied. Cryptographer* cryptographer = session()->context()->directory_manager()->cryptographer(); browser_sync::KeyParams params = {"localhost", "dummy", "foobar"}; cryptographer->AddKey(params); sync_pb::EntitySpecifics specifics; sync_pb::PasswordSpecificsData data; data.set_origin("http://example.com"); cryptographer->Encrypt(data, specifics.MutableExtension(sync_pb::password)->mutable_encrypted()); CreateUnappliedNewItem("item", specifics); apply_updates_command_.ExecuteImpl(session()); sessions::StatusController* status = session()->status_controller(); sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); EXPECT_EQ(1, status->update_progress().AppliedUpdatesSize()) << "All updates should have been attempted"; EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize()) << "No update should be in conflict because they're all decryptable"; EXPECT_EQ(1, status->update_progress().SuccessfullyAppliedUpdateCount()) << "The updates that can be decrypted should be applied"; } TEST_F(ApplyUpdatesCommandTest, UndecryptablePassword) { // Undecryptable password updates should not be applied. sync_pb::EntitySpecifics specifics; specifics.MutableExtension(sync_pb::password); CreateUnappliedNewItem("item", specifics); apply_updates_command_.ExecuteImpl(session()); sessions::StatusController* status = session()->status_controller(); sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); EXPECT_EQ(1, status->update_progress().AppliedUpdatesSize()) << "All updates should have been attempted"; EXPECT_EQ(1, status->conflict_progress().ConflictingItemsSize()) << "The updates that can't be decrypted should be in conflict"; EXPECT_EQ(0, status->update_progress().SuccessfullyAppliedUpdateCount()) << "No update that can't be decrypted should be applied"; } TEST_F(ApplyUpdatesCommandTest, SomeUndecryptablePassword) { // Only decryptable password updates should be applied. { Cryptographer* cryptographer = session()->context()->directory_manager()->cryptographer(); KeyParams params = {"localhost", "dummy", "foobar"}; cryptographer->AddKey(params); sync_pb::EntitySpecifics specifics; sync_pb::PasswordSpecificsData data; data.set_origin("http://example.com/1"); cryptographer->Encrypt(data, specifics.MutableExtension(sync_pb::password)->mutable_encrypted()); CreateUnappliedNewItem("item1", specifics); } { // Create a new cryptographer, independent of the one in the session. Cryptographer cryptographer; KeyParams params = {"localhost", "dummy", "bazqux"}; cryptographer.AddKey(params); sync_pb::EntitySpecifics specifics; sync_pb::PasswordSpecificsData data; data.set_origin("http://example.com/2"); cryptographer.Encrypt(data, specifics.MutableExtension(sync_pb::password)->mutable_encrypted()); CreateUnappliedNewItem("item2", specifics); } apply_updates_command_.ExecuteImpl(session()); sessions::StatusController* status = session()->status_controller(); sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); EXPECT_EQ(2, status->update_progress().AppliedUpdatesSize()) << "All updates should have been attempted"; EXPECT_EQ(1, status->conflict_progress().ConflictingItemsSize()) << "The decryptable password update should be applied"; EXPECT_EQ(1, status->update_progress().SuccessfullyAppliedUpdateCount()) << "The undecryptable password update shouldn't be applied"; } TEST_F(ApplyUpdatesCommandTest, NigoriUpdate) { // Nigori node updates should update the Cryptographer. Cryptographer other_cryptographer; KeyParams params = {"localhost", "dummy", "foobar"}; other_cryptographer.AddKey(params); sync_pb::EntitySpecifics specifics; other_cryptographer.GetKeys( specifics.MutableExtension(sync_pb::nigori)->mutable_encrypted()); CreateUnappliedNewItem("item", specifics); Cryptographer* cryptographer = session()->context()->directory_manager()->cryptographer(); EXPECT_FALSE(cryptographer->has_pending_keys()); apply_updates_command_.ExecuteImpl(session()); sessions::StatusController* status = session()->status_controller(); sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); EXPECT_EQ(1, status->update_progress().AppliedUpdatesSize()) << "All updates should have been attempted"; EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize()) << "The nigori update shouldn't be in conflict"; EXPECT_EQ(1, status->update_progress().SuccessfullyAppliedUpdateCount()) << "The nigori update should be applied"; EXPECT_FALSE(cryptographer->is_ready()); EXPECT_TRUE(cryptographer->has_pending_keys()); } } // namespace browser_sync