// 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 "sync/engine/directory_update_handler.h" #include "base/compiler_specific.h" #include "base/message_loop/message_loop.h" #include "base/stl_util.h" #include "sync/engine/syncer_proto_util.h" #include "sync/internal_api/public/base/model_type.h" #include "sync/internal_api/public/test/test_entry_factory.h" #include "sync/protocol/sync.pb.h" #include "sync/sessions/directory_type_debug_info_emitter.h" #include "sync/sessions/status_controller.h" #include "sync/syncable/directory.h" #include "sync/syncable/entry.h" #include "sync/syncable/mutable_entry.h" #include "sync/syncable/syncable_model_neutral_write_transaction.h" #include "sync/syncable/syncable_proto_util.h" #include "sync/syncable/syncable_read_transaction.h" #include "sync/syncable/syncable_write_transaction.h" #include "sync/test/engine/fake_model_worker.h" #include "sync/test/engine/test_directory_setter_upper.h" #include "sync/test/engine/test_id_factory.h" #include "sync/test/engine/test_syncable_utils.h" #include "testing/gtest/include/gtest/gtest.h" namespace syncer { using syncable::UNITTEST; static const int64 kDefaultVersion = 1000; // A test harness for tests that focus on processing updates. // // Update processing is what occurs when we first download updates. It converts // the received protobuf message into information in the syncable::Directory. // Any invalid or redundant updates will be dropped at this point. class DirectoryUpdateHandlerProcessUpdateTest : public ::testing::Test { public: DirectoryUpdateHandlerProcessUpdateTest() : ui_worker_(new FakeModelWorker(GROUP_UI)) { } virtual ~DirectoryUpdateHandlerProcessUpdateTest() {} virtual void SetUp() OVERRIDE { dir_maker_.SetUp(); } virtual void TearDown() OVERRIDE { dir_maker_.TearDown(); } syncable::Directory* dir() { return dir_maker_.directory(); } protected: scoped_ptr CreateUpdate( const std::string& id, const std::string& parent, const ModelType& type); // This exists mostly to give tests access to the protected member function. // Warning: This takes the syncable directory lock. void UpdateSyncEntities( DirectoryUpdateHandler* handler, const SyncEntityList& applicable_updates, sessions::StatusController* status); // Another function to access private member functions. void UpdateProgressMarkers( DirectoryUpdateHandler* handler, const sync_pb::DataTypeProgressMarker& progress); scoped_refptr ui_worker() { return ui_worker_; } bool EntryExists(const std::string& id) { syncable::ReadTransaction trans(FROM_HERE, dir()); syncable::Entry e(&trans, syncable::GET_BY_ID, syncable::Id::CreateFromServerId(id)); return e.good() && !e.GetIsDel(); } protected: // Used in the construction of DirectoryTypeDebugInfoEmitters. ObserverList type_observers_; private: base::MessageLoop loop_; // Needed to initialize the directory. TestDirectorySetterUpper dir_maker_; scoped_refptr ui_worker_; }; scoped_ptr DirectoryUpdateHandlerProcessUpdateTest::CreateUpdate( const std::string& id, const std::string& parent, const ModelType& type) { scoped_ptr e(new sync_pb::SyncEntity()); e->set_id_string(id); e->set_parent_id_string(parent); e->set_non_unique_name(id); e->set_name(id); e->set_version(kDefaultVersion); AddDefaultFieldValue(type, e->mutable_specifics()); return e.Pass(); } void DirectoryUpdateHandlerProcessUpdateTest::UpdateSyncEntities( DirectoryUpdateHandler* handler, const SyncEntityList& applicable_updates, sessions::StatusController* status) { syncable::ModelNeutralWriteTransaction trans(FROM_HERE, UNITTEST, dir()); handler->UpdateSyncEntities(&trans, applicable_updates, status); } void DirectoryUpdateHandlerProcessUpdateTest::UpdateProgressMarkers( DirectoryUpdateHandler* handler, const sync_pb::DataTypeProgressMarker& progress) { handler->UpdateProgressMarker(progress); } static const char kCacheGuid[] = "IrcjZ2jyzHDV9Io4+zKcXQ=="; // Test that the bookmark tag is set on newly downloaded items. TEST_F(DirectoryUpdateHandlerProcessUpdateTest, NewBookmarkTag) { DirectoryTypeDebugInfoEmitter emitter(BOOKMARKS, &type_observers_); DirectoryUpdateHandler handler(dir(), BOOKMARKS, ui_worker(), &emitter); sync_pb::GetUpdatesResponse gu_response; sessions::StatusController status; // Add a bookmark item to the update message. std::string root = syncable::GetNullId().GetServerId(); syncable::Id server_id = syncable::Id::CreateFromServerId("b1"); scoped_ptr e = CreateUpdate(SyncableIdToProto(server_id), root, BOOKMARKS); e->set_originator_cache_guid( std::string(kCacheGuid, arraysize(kCacheGuid)-1)); syncable::Id client_id = syncable::Id::CreateFromClientString("-2"); e->set_originator_client_item_id(client_id.GetServerId()); e->set_position_in_parent(0); // Add it to the applicable updates list. SyncEntityList bookmark_updates; bookmark_updates.push_back(e.get()); // Process the update. UpdateSyncEntities(&handler, bookmark_updates, &status); syncable::ReadTransaction trans(FROM_HERE, dir()); syncable::Entry entry(&trans, syncable::GET_BY_ID, server_id); ASSERT_TRUE(entry.good()); EXPECT_TRUE(UniquePosition::IsValidSuffix(entry.GetUniqueBookmarkTag())); EXPECT_TRUE(entry.GetServerUniquePosition().IsValid()); // If this assertion fails, that might indicate that the algorithm used to // generate bookmark tags has been modified. This could have implications for // bookmark ordering. Please make sure you know what you're doing if you // intend to make such a change. EXPECT_EQ("6wHRAb3kbnXV5GHrejp4/c1y5tw=", entry.GetUniqueBookmarkTag()); } // Test the receipt of a type root node. TEST_F(DirectoryUpdateHandlerProcessUpdateTest, ReceiveServerCreatedBookmarkFolders) { DirectoryTypeDebugInfoEmitter emitter(BOOKMARKS, &type_observers_); DirectoryUpdateHandler handler(dir(), BOOKMARKS, ui_worker(), &emitter); sync_pb::GetUpdatesResponse gu_response; sessions::StatusController status; // Create an update that mimics the bookmark root. syncable::Id server_id = syncable::Id::CreateFromServerId("xyz"); std::string root = syncable::GetNullId().GetServerId(); scoped_ptr e = CreateUpdate(SyncableIdToProto(server_id), root, BOOKMARKS); e->set_server_defined_unique_tag("google_chrome_bookmarks"); e->set_folder(true); // Add it to the applicable updates list. SyncEntityList bookmark_updates; bookmark_updates.push_back(e.get()); EXPECT_FALSE(SyncerProtoUtil::ShouldMaintainPosition(*e)); // Process it. UpdateSyncEntities(&handler, bookmark_updates, &status); // Verify the results. syncable::ReadTransaction trans(FROM_HERE, dir()); syncable::Entry entry(&trans, syncable::GET_BY_ID, server_id); ASSERT_TRUE(entry.good()); EXPECT_FALSE(entry.ShouldMaintainPosition()); EXPECT_FALSE(entry.GetUniquePosition().IsValid()); EXPECT_FALSE(entry.GetServerUniquePosition().IsValid()); EXPECT_TRUE(entry.GetUniqueBookmarkTag().empty()); } // Test the receipt of a non-bookmark item. TEST_F(DirectoryUpdateHandlerProcessUpdateTest, ReceiveNonBookmarkItem) { DirectoryTypeDebugInfoEmitter emitter(AUTOFILL, &type_observers_); DirectoryUpdateHandler handler(dir(), AUTOFILL, ui_worker(), &emitter); sync_pb::GetUpdatesResponse gu_response; sessions::StatusController status; std::string root = syncable::GetNullId().GetServerId(); syncable::Id server_id = syncable::Id::CreateFromServerId("xyz"); scoped_ptr e = CreateUpdate(SyncableIdToProto(server_id), root, AUTOFILL); e->set_server_defined_unique_tag("9PGRuKdX5sHyGMB17CvYTXuC43I="); // Add it to the applicable updates list. SyncEntityList autofill_updates; autofill_updates.push_back(e.get()); EXPECT_FALSE(SyncerProtoUtil::ShouldMaintainPosition(*e)); // Process it. UpdateSyncEntities(&handler, autofill_updates, &status); syncable::ReadTransaction trans(FROM_HERE, dir()); syncable::Entry entry(&trans, syncable::GET_BY_ID, server_id); ASSERT_TRUE(entry.good()); EXPECT_FALSE(entry.ShouldMaintainPosition()); EXPECT_FALSE(entry.GetUniquePosition().IsValid()); EXPECT_FALSE(entry.GetServerUniquePosition().IsValid()); EXPECT_TRUE(entry.GetUniqueBookmarkTag().empty()); } // Tests the setting of progress markers. TEST_F(DirectoryUpdateHandlerProcessUpdateTest, ProcessNewProgressMarkers) { DirectoryTypeDebugInfoEmitter emitter(BOOKMARKS, &type_observers_); DirectoryUpdateHandler handler(dir(), BOOKMARKS, ui_worker(), &emitter); sync_pb::DataTypeProgressMarker progress; progress.set_data_type_id(GetSpecificsFieldNumberFromModelType(BOOKMARKS)); progress.set_token("token"); UpdateProgressMarkers(&handler, progress); sync_pb::DataTypeProgressMarker saved; dir()->GetDownloadProgress(BOOKMARKS, &saved); EXPECT_EQ(progress.token(), saved.token()); EXPECT_EQ(progress.data_type_id(), saved.data_type_id()); } TEST_F(DirectoryUpdateHandlerProcessUpdateTest, GarbageCollectionByVersion) { DirectoryTypeDebugInfoEmitter emitter(SYNCED_NOTIFICATIONS, &type_observers_); DirectoryUpdateHandler handler(dir(), SYNCED_NOTIFICATIONS, ui_worker(), &emitter); sessions::StatusController status; sync_pb::DataTypeProgressMarker progress; progress.set_data_type_id( GetSpecificsFieldNumberFromModelType(SYNCED_NOTIFICATIONS)); progress.set_token("token"); progress.mutable_gc_directive()->set_version_watermark(kDefaultVersion + 10); sync_pb::DataTypeContext context; context.set_data_type_id( GetSpecificsFieldNumberFromModelType(SYNCED_NOTIFICATIONS)); context.set_context("context"); context.set_version(1); scoped_ptr type_root = CreateUpdate(SyncableIdToProto(syncable::Id::CreateFromServerId("root")), syncable::GetNullId().GetServerId(), SYNCED_NOTIFICATIONS); type_root->set_server_defined_unique_tag( ModelTypeToRootTag(SYNCED_NOTIFICATIONS)); type_root->set_folder(true); scoped_ptr e1 = CreateUpdate(SyncableIdToProto(syncable::Id::CreateFromServerId("e1")), type_root->id_string(), SYNCED_NOTIFICATIONS); scoped_ptr e2 = CreateUpdate(SyncableIdToProto(syncable::Id::CreateFromServerId("e2")), type_root->id_string(), SYNCED_NOTIFICATIONS); e2->set_version(kDefaultVersion + 100); // Add to the applicable updates list. SyncEntityList updates; updates.push_back(type_root.get()); updates.push_back(e1.get()); updates.push_back(e2.get()); // Process and apply updates. EXPECT_EQ( SYNCER_OK, handler.ProcessGetUpdatesResponse(progress, context, updates, &status)); handler.ApplyUpdates(&status); // Verify none is deleted because they are unapplied during GC. EXPECT_TRUE(EntryExists(type_root->id_string())); EXPECT_TRUE(EntryExists(e1->id_string())); EXPECT_TRUE(EntryExists(e2->id_string())); // Process and apply again. Old entry is deleted but not root. progress.mutable_gc_directive()->set_version_watermark(kDefaultVersion + 20); EXPECT_EQ(SYNCER_OK, handler.ProcessGetUpdatesResponse( progress, context, SyncEntityList(), &status)); handler.ApplyUpdates(&status); EXPECT_TRUE(EntryExists(type_root->id_string())); EXPECT_FALSE(EntryExists(e1->id_string())); EXPECT_TRUE(EntryExists(e2->id_string())); } TEST_F(DirectoryUpdateHandlerProcessUpdateTest, ContextVersion) { DirectoryTypeDebugInfoEmitter emitter(SYNCED_NOTIFICATIONS, &type_observers_); DirectoryUpdateHandler handler(dir(), SYNCED_NOTIFICATIONS, ui_worker(), &emitter); sessions::StatusController status; int field_number = GetSpecificsFieldNumberFromModelType(SYNCED_NOTIFICATIONS); sync_pb::DataTypeProgressMarker progress; progress.set_data_type_id( GetSpecificsFieldNumberFromModelType(SYNCED_NOTIFICATIONS)); progress.set_token("token"); sync_pb::DataTypeContext old_context; old_context.set_version(1); old_context.set_context("data"); old_context.set_data_type_id(field_number); scoped_ptr type_root = CreateUpdate(SyncableIdToProto(syncable::Id::CreateFromServerId("root")), syncable::GetNullId().GetServerId(), SYNCED_NOTIFICATIONS); type_root->set_server_defined_unique_tag( ModelTypeToRootTag(SYNCED_NOTIFICATIONS)); type_root->set_folder(true); scoped_ptr e1 = CreateUpdate(SyncableIdToProto(syncable::Id::CreateFromServerId("e1")), type_root->id_string(), SYNCED_NOTIFICATIONS); SyncEntityList updates; updates.push_back(type_root.get()); updates.push_back(e1.get()); // The first response should be processed fine. EXPECT_EQ(SYNCER_OK, handler.ProcessGetUpdatesResponse( progress, old_context, updates, &status)); handler.ApplyUpdates(&status); EXPECT_TRUE(EntryExists(type_root->id_string())); EXPECT_TRUE(EntryExists(e1->id_string())); { sync_pb::DataTypeContext dir_context; syncable::ReadTransaction trans(FROM_HERE, dir()); trans.directory()->GetDataTypeContext( &trans, SYNCED_NOTIFICATIONS, &dir_context); EXPECT_EQ(old_context.SerializeAsString(), dir_context.SerializeAsString()); } sync_pb::DataTypeContext new_context; new_context.set_version(0); new_context.set_context("old"); new_context.set_data_type_id(field_number); scoped_ptr e2 = CreateUpdate(SyncableIdToProto(syncable::Id::CreateFromServerId("e2")), type_root->id_string(), SYNCED_NOTIFICATIONS); updates.clear(); updates.push_back(e2.get()); // The second response, with an old context version, should result in an // error and the updates should be dropped. EXPECT_EQ(DATATYPE_TRIGGERED_RETRY, handler.ProcessGetUpdatesResponse( progress, new_context, updates, &status)); handler.ApplyUpdates(&status); EXPECT_FALSE(EntryExists(e2->id_string())); { sync_pb::DataTypeContext dir_context; syncable::ReadTransaction trans(FROM_HERE, dir()); trans.directory()->GetDataTypeContext( &trans, SYNCED_NOTIFICATIONS, &dir_context); EXPECT_EQ(old_context.SerializeAsString(), dir_context.SerializeAsString()); } } // A test harness for tests that focus on applying updates. // // Update application is performed when we want to take updates that were // previously downloaded, processed, and stored in our syncable::Directory // and use them to update our local state (both the Directory's local state // and the model's local state, though these tests focus only on the Directory's // local state). // // This is kept separate from the update processing test in part for historical // reasons, and in part because these tests may require a bit more infrastrcture // in the future. Update application should happen on a different thread a lot // of the time so these tests may end up requiring more infrastructure than the // update processing tests. Currently, we're bypassing most of those issues by // using FakeModelWorkers, so there's not much difference between the two test // harnesses. class DirectoryUpdateHandlerApplyUpdateTest : public ::testing::Test { public: DirectoryUpdateHandlerApplyUpdateTest() : ui_worker_(new FakeModelWorker(GROUP_UI)), password_worker_(new FakeModelWorker(GROUP_PASSWORD)), passive_worker_(new FakeModelWorker(GROUP_PASSIVE)), bookmarks_emitter_(BOOKMARKS, &type_observers_), passwords_emitter_(PASSWORDS, &type_observers_), update_handler_map_deleter_(&update_handler_map_) {} virtual void SetUp() OVERRIDE { dir_maker_.SetUp(); entry_factory_.reset(new TestEntryFactory(directory())); update_handler_map_.insert(std::make_pair( BOOKMARKS, new DirectoryUpdateHandler(directory(), BOOKMARKS, ui_worker_, &bookmarks_emitter_))); update_handler_map_.insert(std::make_pair( PASSWORDS, new DirectoryUpdateHandler(directory(), PASSWORDS, password_worker_, &passwords_emitter_))); } virtual void TearDown() OVERRIDE { dir_maker_.TearDown(); } const UpdateCounters& GetBookmarksUpdateCounters() { return bookmarks_emitter_.GetUpdateCounters(); } const UpdateCounters& GetPasswordsUpdateCounters() { return passwords_emitter_.GetUpdateCounters(); } protected: void ApplyBookmarkUpdates(sessions::StatusController* status) { update_handler_map_[BOOKMARKS]->ApplyUpdates(status); } void ApplyPasswordUpdates(sessions::StatusController* status) { update_handler_map_[PASSWORDS]->ApplyUpdates(status); } TestEntryFactory* entry_factory() { return entry_factory_.get(); } syncable::Directory* directory() { return dir_maker_.directory(); } private: typedef std::map UpdateHandlerMap; base::MessageLoop loop_; // Needed to initialize the directory. TestDirectorySetterUpper dir_maker_; scoped_ptr entry_factory_; scoped_refptr ui_worker_; scoped_refptr password_worker_; scoped_refptr passive_worker_; ObserverList type_observers_; DirectoryTypeDebugInfoEmitter bookmarks_emitter_; DirectoryTypeDebugInfoEmitter passwords_emitter_; UpdateHandlerMap update_handler_map_; STLValueDeleter update_handler_map_deleter_; }; namespace { sync_pb::EntitySpecifics DefaultBookmarkSpecifics() { sync_pb::EntitySpecifics result; AddDefaultFieldValue(BOOKMARKS, &result); return result; } } // namespace // Test update application for a few bookmark items. TEST_F(DirectoryUpdateHandlerApplyUpdateTest, SimpleBookmark) { sessions::StatusController status; std::string root_server_id = syncable::GetNullId().GetServerId(); int64 parent_handle = entry_factory()->CreateUnappliedNewBookmarkItemWithParent( "parent", DefaultBookmarkSpecifics(), root_server_id); int64 child_handle = entry_factory()->CreateUnappliedNewBookmarkItemWithParent( "child", DefaultBookmarkSpecifics(), "parent"); ApplyBookmarkUpdates(&status); const UpdateCounters& counter = GetBookmarksUpdateCounters(); EXPECT_EQ(0, counter.num_encryption_conflict_application_failures) << "Simple update shouldn't result in conflicts"; EXPECT_EQ(0, counter.num_hierarchy_conflict_application_failures) << "Simple update shouldn't result in conflicts"; EXPECT_EQ(2, counter.num_updates_applied) << "All items should have been successfully applied"; { syncable::ReadTransaction trans(FROM_HERE, directory()); syncable::Entry parent(&trans, syncable::GET_BY_HANDLE, parent_handle); syncable::Entry child(&trans, syncable::GET_BY_HANDLE, child_handle); ASSERT_TRUE(parent.good()); ASSERT_TRUE(child.good()); EXPECT_FALSE(parent.GetIsUnsynced()); EXPECT_FALSE(parent.GetIsUnappliedUpdate()); EXPECT_FALSE(child.GetIsUnsynced()); EXPECT_FALSE(child.GetIsUnappliedUpdate()); } } // Test that the applicator can handle updates delivered out of order. TEST_F(DirectoryUpdateHandlerApplyUpdateTest, BookmarkChildrenBeforeParent) { // Start with some bookmarks whose parents are unknown. std::string root_server_id = syncable::GetNullId().GetServerId(); int64 a_handle = entry_factory()->CreateUnappliedNewBookmarkItemWithParent( "a_child_created_first", DefaultBookmarkSpecifics(), "parent"); int64 x_handle = entry_factory()->CreateUnappliedNewBookmarkItemWithParent( "x_child_created_first", DefaultBookmarkSpecifics(), "parent"); // Update application will fail. sessions::StatusController status1; ApplyBookmarkUpdates(&status1); EXPECT_EQ(0, status1.num_updates_applied()); EXPECT_EQ(2, status1.num_hierarchy_conflicts()); { syncable::ReadTransaction trans(FROM_HERE, directory()); syncable::Entry a(&trans, syncable::GET_BY_HANDLE, a_handle); syncable::Entry x(&trans, syncable::GET_BY_HANDLE, x_handle); ASSERT_TRUE(a.good()); ASSERT_TRUE(x.good()); EXPECT_TRUE(a.GetIsUnappliedUpdate()); EXPECT_TRUE(x.GetIsUnappliedUpdate()); } // Now add their parent and a few siblings. entry_factory()->CreateUnappliedNewBookmarkItemWithParent( "parent", DefaultBookmarkSpecifics(), root_server_id); entry_factory()->CreateUnappliedNewBookmarkItemWithParent( "a_child_created_second", DefaultBookmarkSpecifics(), "parent"); entry_factory()->CreateUnappliedNewBookmarkItemWithParent( "x_child_created_second", DefaultBookmarkSpecifics(), "parent"); // Update application will succeed. sessions::StatusController status2; ApplyBookmarkUpdates(&status2); EXPECT_EQ(5, status2.num_updates_applied()) << "All updates should have been successfully applied"; { syncable::ReadTransaction trans(FROM_HERE, directory()); syncable::Entry a(&trans, syncable::GET_BY_HANDLE, a_handle); syncable::Entry x(&trans, syncable::GET_BY_HANDLE, x_handle); ASSERT_TRUE(a.good()); ASSERT_TRUE(x.good()); EXPECT_FALSE(a.GetIsUnappliedUpdate()); EXPECT_FALSE(x.GetIsUnappliedUpdate()); } } // Try to apply changes on an item that is both IS_UNSYNCED and // IS_UNAPPLIED_UPDATE. Conflict resolution should be performed. TEST_F(DirectoryUpdateHandlerApplyUpdateTest, SimpleBookmarkConflict) { int64 handle = entry_factory()->CreateUnappliedAndUnsyncedBookmarkItem("x"); int original_server_version = -10; { syncable::ReadTransaction trans(FROM_HERE, directory()); syncable::Entry e(&trans, syncable::GET_BY_HANDLE, handle); original_server_version = e.GetServerVersion(); ASSERT_NE(original_server_version, e.GetBaseVersion()); EXPECT_TRUE(e.GetIsUnsynced()); } sessions::StatusController status; ApplyBookmarkUpdates(&status); const UpdateCounters& counters = GetBookmarksUpdateCounters(); EXPECT_EQ(1, counters.num_server_overwrites) << "Unsynced and unapplied item conflict should be resolved"; EXPECT_EQ(0, counters.num_updates_applied) << "Update should not be applied; we should override the server."; { syncable::ReadTransaction trans(FROM_HERE, directory()); syncable::Entry e(&trans, syncable::GET_BY_HANDLE, handle); ASSERT_TRUE(e.good()); EXPECT_EQ(original_server_version, e.GetServerVersion()); EXPECT_EQ(original_server_version, e.GetBaseVersion()); EXPECT_FALSE(e.GetIsUnappliedUpdate()); // The unsynced flag will remain set until we successfully commit the item. EXPECT_TRUE(e.GetIsUnsynced()); } } // Create a simple conflict that is also a hierarchy conflict. If we were to // follow the normal "server wins" logic, we'd end up violating hierarchy // constraints. The hierarchy conflict must take precedence. We can not allow // the update to be applied. The item must remain in the conflict state. TEST_F(DirectoryUpdateHandlerApplyUpdateTest, HierarchyAndSimpleConflict) { // Create a simply-conflicting item. It will start with valid parent ids. int64 handle = entry_factory()->CreateUnappliedAndUnsyncedBookmarkItem( "orphaned_by_server"); { // Manually set the SERVER_PARENT_ID to bad value. // A bad parent indicates a hierarchy conflict. syncable::WriteTransaction trans(FROM_HERE, UNITTEST, directory()); syncable::MutableEntry entry(&trans, syncable::GET_BY_HANDLE, handle); ASSERT_TRUE(entry.good()); entry.PutServerParentId(TestIdFactory::MakeServer("bogus_parent")); } sessions::StatusController status; ApplyBookmarkUpdates(&status); const UpdateCounters& counters = GetBookmarksUpdateCounters(); EXPECT_EQ(0, counters.num_updates_applied); EXPECT_EQ(0, counters.num_server_overwrites); EXPECT_EQ(1, counters.num_hierarchy_conflict_application_failures); { syncable::ReadTransaction trans(FROM_HERE, directory()); syncable::Entry e(&trans, syncable::GET_BY_HANDLE, handle); ASSERT_TRUE(e.good()); EXPECT_TRUE(e.GetIsUnappliedUpdate()); EXPECT_TRUE(e.GetIsUnsynced()); } } // Attempt to apply an udpate that would create a bookmark folder loop. This // application should fail. TEST_F(DirectoryUpdateHandlerApplyUpdateTest, BookmarkFolderLoop) { // Item 'X' locally has parent of 'root'. Server is updating it to have // parent of 'Y'. // Create it as a child of root node. int64 handle = entry_factory()->CreateSyncedItem("X", BOOKMARKS, true); { syncable::WriteTransaction trans(FROM_HERE, UNITTEST, directory()); syncable::MutableEntry entry(&trans, syncable::GET_BY_HANDLE, handle); ASSERT_TRUE(entry.good()); // Re-parent from root to "Y" entry.PutServerVersion(entry_factory()->GetNextRevision()); entry.PutIsUnappliedUpdate(true); entry.PutServerParentId(TestIdFactory::MakeServer("Y")); } // Item 'Y' is child of 'X'. entry_factory()->CreateUnsyncedItem( TestIdFactory::MakeServer("Y"), TestIdFactory::MakeServer("X"), "Y", true, BOOKMARKS, NULL); // If the server's update were applied, we would have X be a child of Y, and Y // as a child of X. That's a directory loop. The UpdateApplicator should // prevent the update from being applied and note that this is a hierarchy // conflict. sessions::StatusController status; ApplyBookmarkUpdates(&status); // This should count as a hierarchy conflict. const UpdateCounters& counters = GetBookmarksUpdateCounters(); EXPECT_EQ(1, counters.num_hierarchy_conflict_application_failures); { syncable::ReadTransaction trans(FROM_HERE, directory()); syncable::Entry e(&trans, syncable::GET_BY_HANDLE, handle); ASSERT_TRUE(e.good()); EXPECT_TRUE(e.GetIsUnappliedUpdate()); EXPECT_FALSE(e.GetIsUnsynced()); } } // Test update application where the update has been orphaned by a local folder // deletion. The update application attempt should fail. TEST_F(DirectoryUpdateHandlerApplyUpdateTest, HierarchyConflictDeletedParent) { // Create a locally deleted parent item. int64 parent_handle; entry_factory()->CreateUnsyncedItem( syncable::Id::CreateFromServerId("parent"), TestIdFactory::root(), "parent", true, BOOKMARKS, &parent_handle); { syncable::WriteTransaction trans(FROM_HERE, UNITTEST, directory()); syncable::MutableEntry entry(&trans, syncable::GET_BY_HANDLE, parent_handle); entry.PutIsDel(true); } // Create an incoming child from the server. int64 child_handle = entry_factory()->CreateUnappliedNewItemWithParent( "child", DefaultBookmarkSpecifics(), "parent"); // The server's update may seem valid to some other client, but on this client // that new item's parent no longer exists. The update should not be applied // and the update applicator should indicate this is a hierarchy conflict. sessions::StatusController status; ApplyBookmarkUpdates(&status); const UpdateCounters& counters = GetBookmarksUpdateCounters(); EXPECT_EQ(1, counters.num_hierarchy_conflict_application_failures); { syncable::ReadTransaction trans(FROM_HERE, directory()); syncable::Entry child(&trans, syncable::GET_BY_HANDLE, child_handle); ASSERT_TRUE(child.good()); EXPECT_TRUE(child.GetIsUnappliedUpdate()); EXPECT_FALSE(child.GetIsUnsynced()); } } // Attempt to apply an update that deletes a folder where the folder has // locally-created children. The update application should fail. TEST_F(DirectoryUpdateHandlerApplyUpdateTest, HierarchyConflictDeleteNonEmptyDirectory) { // Create a server-deleted folder as a child of root node. int64 parent_handle = entry_factory()->CreateSyncedItem("parent", BOOKMARKS, true); { syncable::WriteTransaction trans(FROM_HERE, UNITTEST, directory()); syncable::MutableEntry entry(&trans, syncable::GET_BY_HANDLE, parent_handle); ASSERT_TRUE(entry.good()); // Delete it on the server. entry.PutServerVersion(entry_factory()->GetNextRevision()); entry.PutIsUnappliedUpdate(true); entry.PutServerParentId(TestIdFactory::root()); entry.PutServerIsDel(true); } // Create a local child of the server-deleted directory. entry_factory()->CreateUnsyncedItem( TestIdFactory::MakeServer("child"), TestIdFactory::MakeServer("parent"), "child", false, BOOKMARKS, NULL); // The server's request to delete the directory must be ignored, otherwise our // unsynced new child would be orphaned. This is a hierarchy conflict. sessions::StatusController status; ApplyBookmarkUpdates(&status); // This should count as a hierarchy conflict. const UpdateCounters& counters = GetBookmarksUpdateCounters(); EXPECT_EQ(1, counters.num_hierarchy_conflict_application_failures); { syncable::ReadTransaction trans(FROM_HERE, directory()); syncable::Entry parent(&trans, syncable::GET_BY_HANDLE, parent_handle); ASSERT_TRUE(parent.good()); EXPECT_TRUE(parent.GetIsUnappliedUpdate()); EXPECT_FALSE(parent.GetIsUnsynced()); } } // Attempt to apply updates where the updated item's parent is not known to this // client. The update application attempt should fail. TEST_F(DirectoryUpdateHandlerApplyUpdateTest, HierarchyConflictUnknownParent) { // We shouldn't be able to do anything with either of these items. int64 x_handle = entry_factory()->CreateUnappliedNewItemWithParent( "some_item", DefaultBookmarkSpecifics(), "unknown_parent"); int64 y_handle = entry_factory()->CreateUnappliedNewItemWithParent( "some_other_item", DefaultBookmarkSpecifics(), "some_item"); sessions::StatusController status; ApplyBookmarkUpdates(&status); const UpdateCounters& counters = GetBookmarksUpdateCounters(); EXPECT_EQ(2, counters.num_hierarchy_conflict_application_failures) << "All updates with an unknown ancestors should be in conflict"; EXPECT_EQ(0, counters.num_updates_applied) << "No item with an unknown ancestor should be applied"; { syncable::ReadTransaction trans(FROM_HERE, directory()); syncable::Entry x(&trans, syncable::GET_BY_HANDLE, x_handle); syncable::Entry y(&trans, syncable::GET_BY_HANDLE, y_handle); ASSERT_TRUE(x.good()); ASSERT_TRUE(y.good()); EXPECT_TRUE(x.GetIsUnappliedUpdate()); EXPECT_TRUE(y.GetIsUnappliedUpdate()); EXPECT_FALSE(x.GetIsUnsynced()); EXPECT_FALSE(y.GetIsUnsynced()); } } // Attempt application of a mix of items. Some update application attempts will // fail due to hierarchy conflicts. Others should succeed. TEST_F(DirectoryUpdateHandlerApplyUpdateTest, ItemsBothKnownAndUnknown) { // See what happens when there's a mixture of good and bad updates. std::string root_server_id = syncable::GetNullId().GetServerId(); int64 u1_handle = entry_factory()->CreateUnappliedNewItemWithParent( "first_unknown_item", DefaultBookmarkSpecifics(), "unknown_parent"); int64 k1_handle = entry_factory()->CreateUnappliedNewItemWithParent( "first_known_item", DefaultBookmarkSpecifics(), root_server_id); int64 u2_handle = entry_factory()->CreateUnappliedNewItemWithParent( "second_unknown_item", DefaultBookmarkSpecifics(), "unknown_parent"); int64 k2_handle = entry_factory()->CreateUnappliedNewItemWithParent( "second_known_item", DefaultBookmarkSpecifics(), "first_known_item"); int64 k3_handle = entry_factory()->CreateUnappliedNewItemWithParent( "third_known_item", DefaultBookmarkSpecifics(), "fourth_known_item"); int64 k4_handle = entry_factory()->CreateUnappliedNewItemWithParent( "fourth_known_item", DefaultBookmarkSpecifics(), root_server_id); sessions::StatusController status; ApplyBookmarkUpdates(&status); const UpdateCounters& counters = GetBookmarksUpdateCounters(); EXPECT_EQ(2, counters.num_hierarchy_conflict_application_failures) << "The updates with unknown ancestors should be in conflict"; EXPECT_EQ(4, counters.num_updates_applied) << "The updates with known ancestors should be successfully applied"; { syncable::ReadTransaction trans(FROM_HERE, directory()); syncable::Entry u1(&trans, syncable::GET_BY_HANDLE, u1_handle); syncable::Entry u2(&trans, syncable::GET_BY_HANDLE, u2_handle); syncable::Entry k1(&trans, syncable::GET_BY_HANDLE, k1_handle); syncable::Entry k2(&trans, syncable::GET_BY_HANDLE, k2_handle); syncable::Entry k3(&trans, syncable::GET_BY_HANDLE, k3_handle); syncable::Entry k4(&trans, syncable::GET_BY_HANDLE, k4_handle); ASSERT_TRUE(u1.good()); ASSERT_TRUE(u2.good()); ASSERT_TRUE(k1.good()); ASSERT_TRUE(k2.good()); ASSERT_TRUE(k3.good()); ASSERT_TRUE(k4.good()); EXPECT_TRUE(u1.GetIsUnappliedUpdate()); EXPECT_TRUE(u2.GetIsUnappliedUpdate()); EXPECT_FALSE(k1.GetIsUnappliedUpdate()); EXPECT_FALSE(k2.GetIsUnappliedUpdate()); EXPECT_FALSE(k3.GetIsUnappliedUpdate()); EXPECT_FALSE(k4.GetIsUnappliedUpdate()); } } // Attempt application of password upates where the passphrase is known. TEST_F(DirectoryUpdateHandlerApplyUpdateTest, DecryptablePassword) { // Decryptable password updates should be applied. Cryptographer* cryptographer; { // Storing the cryptographer separately is bad, but for this test we // know it's safe. syncable::ReadTransaction trans(FROM_HERE, directory()); cryptographer = directory()->GetCryptographer(&trans); } 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.mutable_password()->mutable_encrypted()); int64 handle = entry_factory()->CreateUnappliedNewItem("item", specifics, false); sessions::StatusController status; ApplyPasswordUpdates(&status); const UpdateCounters& counters = GetPasswordsUpdateCounters(); EXPECT_EQ(1, counters.num_updates_applied) << "The updates that can be decrypted should be applied"; { syncable::ReadTransaction trans(FROM_HERE, directory()); syncable::Entry e(&trans, syncable::GET_BY_HANDLE, handle); ASSERT_TRUE(e.good()); EXPECT_FALSE(e.GetIsUnappliedUpdate()); EXPECT_FALSE(e.GetIsUnsynced()); } } // Attempt application of encrypted items when the passphrase is not known. TEST_F(DirectoryUpdateHandlerApplyUpdateTest, UndecryptableData) { // Undecryptable updates should not be applied. sync_pb::EntitySpecifics encrypted_bookmark; encrypted_bookmark.mutable_encrypted(); AddDefaultFieldValue(BOOKMARKS, &encrypted_bookmark); std::string root_server_id = syncable::GetNullId().GetServerId(); int64 folder_handle = entry_factory()->CreateUnappliedNewItemWithParent( "folder", encrypted_bookmark, root_server_id); int64 bookmark_handle = entry_factory()->CreateUnappliedNewItem( "item2", encrypted_bookmark, false); sync_pb::EntitySpecifics encrypted_password; encrypted_password.mutable_password(); int64 password_handle = entry_factory()->CreateUnappliedNewItem( "item3", encrypted_password, false); sessions::StatusController status; ApplyBookmarkUpdates(&status); ApplyPasswordUpdates(&status); const UpdateCounters& bm_counters = GetBookmarksUpdateCounters(); EXPECT_EQ(2, bm_counters.num_encryption_conflict_application_failures) << "Updates that can't be decrypted should be in encryption conflict"; EXPECT_EQ(0, bm_counters.num_updates_applied) << "No update that can't be decrypted should be applied"; const UpdateCounters& pw_counters = GetPasswordsUpdateCounters(); EXPECT_EQ(1, pw_counters.num_encryption_conflict_application_failures) << "Updates that can't be decrypted should be in encryption conflict"; EXPECT_EQ(0, pw_counters.num_updates_applied) << "No update that can't be decrypted should be applied"; { syncable::ReadTransaction trans(FROM_HERE, directory()); syncable::Entry folder(&trans, syncable::GET_BY_HANDLE, folder_handle); syncable::Entry bm(&trans, syncable::GET_BY_HANDLE, bookmark_handle); syncable::Entry pw(&trans, syncable::GET_BY_HANDLE, password_handle); ASSERT_TRUE(folder.good()); ASSERT_TRUE(bm.good()); ASSERT_TRUE(pw.good()); EXPECT_TRUE(folder.GetIsUnappliedUpdate()); EXPECT_TRUE(bm.GetIsUnappliedUpdate()); EXPECT_TRUE(pw.GetIsUnappliedUpdate()); } } // Test a mix of decryptable and undecryptable updates. TEST_F(DirectoryUpdateHandlerApplyUpdateTest, SomeUndecryptablePassword) { Cryptographer* cryptographer; int64 decryptable_handle = -1; int64 undecryptable_handle = -1; // Only decryptable password updates should be applied. { sync_pb::EntitySpecifics specifics; sync_pb::PasswordSpecificsData data; data.set_origin("http://example.com/1"); { syncable::ReadTransaction trans(FROM_HERE, directory()); cryptographer = directory()->GetCryptographer(&trans); KeyParams params = {"localhost", "dummy", "foobar"}; cryptographer->AddKey(params); cryptographer->Encrypt(data, specifics.mutable_password()->mutable_encrypted()); } decryptable_handle = entry_factory()->CreateUnappliedNewItem("item1", specifics, false); } { // Create a new cryptographer, independent of the one in the session. Cryptographer other_cryptographer(cryptographer->encryptor()); KeyParams params = {"localhost", "dummy", "bazqux"}; other_cryptographer.AddKey(params); sync_pb::EntitySpecifics specifics; sync_pb::PasswordSpecificsData data; data.set_origin("http://example.com/2"); other_cryptographer.Encrypt(data, specifics.mutable_password()->mutable_encrypted()); undecryptable_handle = entry_factory()->CreateUnappliedNewItem("item2", specifics, false); } sessions::StatusController status; ApplyPasswordUpdates(&status); const UpdateCounters& counters = GetPasswordsUpdateCounters(); EXPECT_EQ(1, counters.num_encryption_conflict_application_failures) << "The updates that can't be decrypted should be in encryption " << "conflict"; EXPECT_EQ(1, counters.num_updates_applied) << "The undecryptable password update shouldn't be applied"; { syncable::ReadTransaction trans(FROM_HERE, directory()); syncable::Entry e1(&trans, syncable::GET_BY_HANDLE, decryptable_handle); syncable::Entry e2(&trans, syncable::GET_BY_HANDLE, undecryptable_handle); ASSERT_TRUE(e1.good()); ASSERT_TRUE(e2.good()); EXPECT_FALSE(e1.GetIsUnappliedUpdate()); EXPECT_TRUE(e2.GetIsUnappliedUpdate()); } } } // namespace syncer