| // Copyright 2012 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "base/files/file_path.h" | |
| #include "base/files/file_util.h" | |
| #include "base/test/task_environment.h" | |
| #include "base/uuid.h" | |
| #include "components/sync/base/client_tag_hash.h" | |
| #include "components/sync/engine/loopback_server/loopback_connection_manager.h" | |
| #include "components/sync/engine/syncer_proto_util.h" | |
| #include "components/sync/protocol/entity_specifics.pb.h" | |
| #include "components/sync/protocol/sync.pb.h" | |
| #include "components/sync/protocol/sync_entity.pb.h" | |
| #include "components/sync/protocol/sync_enums.pb.h" | |
| #include "testing/gtest/include/gtest/gtest.h" | |
| using sync_pb::ClientToServerMessage; | |
| using sync_pb::ClientToServerResponse; | |
| using sync_pb::EntitySpecifics; | |
| using sync_pb::SyncEntity; | |
| using sync_pb::SyncEnums; | |
| namespace syncer { | |
| namespace { | |
| const char kUrl1[] = "http://www.one.com"; | |
| const char kUrl2[] = "http://www.two.com"; | |
| const char kUrl3[] = "http://www.three.com"; | |
| const char kBookmarkBar[] = "bookmark_bar"; | |
| SyncEntity NewBookmarkEntity(const std::string& url, | |
| const std::string& parent_id) { | |
| SyncEntity entity; | |
| entity.mutable_specifics()->mutable_bookmark()->set_url(url); | |
| entity.set_parent_id_string(parent_id); | |
| entity.set_id_string(base::Uuid::GenerateRandomV4().AsLowercaseString()); | |
| return entity; | |
| } | |
| SyncEntity UpdatedBookmarkEntity(const std::string& url, | |
| const std::string& id, | |
| const std::string& parent_id, | |
| int version) { | |
| SyncEntity entity; | |
| entity.mutable_specifics()->mutable_bookmark()->set_url(url); | |
| entity.set_id_string(id); | |
| entity.set_parent_id_string(parent_id); | |
| entity.set_version(version); | |
| return entity; | |
| } | |
| SyncEntity DeletedBookmarkEntity(const std::string& id, int version) { | |
| SyncEntity entity; | |
| entity.mutable_specifics()->mutable_bookmark(); | |
| entity.set_id_string(id); | |
| entity.set_deleted(true); | |
| entity.set_version(version); | |
| return entity; | |
| } | |
| std::map<std::string, SyncEntity> ResponseToMap( | |
| const ClientToServerResponse& response) { | |
| EXPECT_TRUE(response.has_get_updates()); | |
| std::map<std::string, SyncEntity> results; | |
| for (const SyncEntity& entity : response.get_updates().entries()) { | |
| results[entity.id_string()] = entity; | |
| } | |
| return results; | |
| } | |
| } // namespace | |
| class LoopbackServerTest : public testing::Test { | |
| public: | |
| void SetUp() override { | |
| base::CreateTemporaryFile(&persistent_file_); | |
| lcm_ = std::make_unique<LoopbackConnectionManager>(persistent_file_); | |
| } | |
| static bool CallPostAndProcessHeaders(ServerConnectionManager* scm, | |
| const ClientToServerMessage& msg, | |
| ClientToServerResponse* response) { | |
| return SyncerProtoUtil::PostAndProcessHeaders(scm, msg, response, | |
| signin::AccessTokenInfo()); | |
| } | |
| protected: | |
| ClientToServerResponse GetUpdatesForType( | |
| int field_number, | |
| std::string_view birthday = "", | |
| std::string_view progress_token = "") { | |
| ClientToServerMessage request; | |
| SyncerProtoUtil::SetProtocolVersion(&request); | |
| request.set_share("required"); | |
| if (!birthday.empty()) { | |
| request.set_store_birthday(birthday); | |
| } | |
| request.set_message_contents(ClientToServerMessage::GET_UPDATES); | |
| request.mutable_get_updates()->add_from_progress_marker()->set_data_type_id( | |
| field_number); | |
| if (!progress_token.empty()) { | |
| request.mutable_get_updates()->mutable_from_progress_marker(0)->set_token( | |
| progress_token); | |
| } | |
| ClientToServerResponse response; | |
| EXPECT_TRUE(CallPostAndProcessHeaders(lcm_.get(), request, &response)); | |
| EXPECT_EQ(SyncEnums::SUCCESS, response.error_code()); | |
| return response; | |
| } | |
| ClientToServerMessage SingleEntryCommit( | |
| const std::vector<SyncEntity>& entity_vector) { | |
| ClientToServerMessage request; | |
| SyncerProtoUtil::SetProtocolVersion(&request); | |
| request.set_share("required"); | |
| request.set_message_contents(ClientToServerMessage::COMMIT); | |
| request.set_invalidator_client_id("client_id"); | |
| sync_pb::CommitMessage* commit = request.mutable_commit(); | |
| commit->set_cache_guid("cache_guid"); | |
| for (const SyncEntity& entity : entity_vector) { | |
| *commit->add_entries() = entity; | |
| } | |
| return request; | |
| } | |
| std::string CommitVerifySuccess(const SyncEntity& entity) { | |
| ClientToServerMessage request = SingleEntryCommit({entity}); | |
| ClientToServerResponse response; | |
| EXPECT_TRUE(CallPostAndProcessHeaders(lcm_.get(), request, &response)); | |
| EXPECT_EQ(SyncEnums::SUCCESS, response.error_code()); | |
| EXPECT_TRUE(response.has_commit()); | |
| return response.commit().entryresponse(0).id_string(); | |
| } | |
| void CommitVerifyFailure(const SyncEntity& entity) { | |
| ClientToServerMessage request = SingleEntryCommit({entity}); | |
| ClientToServerResponse response; | |
| EXPECT_FALSE(CallPostAndProcessHeaders(lcm_.get(), request, &response)); | |
| EXPECT_NE(SyncEnums::SUCCESS, response.error_code()); | |
| EXPECT_FALSE(response.has_commit()); | |
| } | |
| base::test::TaskEnvironment task_environment_; | |
| base::FilePath persistent_file_; | |
| std::unique_ptr<LoopbackConnectionManager> lcm_; | |
| }; | |
| TEST_F(LoopbackServerTest, WrongBirthday) { | |
| ClientToServerMessage msg; | |
| SyncerProtoUtil::SetProtocolVersion(&msg); | |
| msg.set_share("required"); | |
| msg.set_store_birthday("not_your_birthday"); | |
| msg.set_message_contents(ClientToServerMessage::GET_UPDATES); | |
| msg.mutable_get_updates()->add_from_progress_marker()->set_data_type_id( | |
| EntitySpecifics::kBookmarkFieldNumber); | |
| ClientToServerResponse response; | |
| EXPECT_TRUE(CallPostAndProcessHeaders(lcm_.get(), msg, &response)); | |
| EXPECT_EQ(SyncEnums::NOT_MY_BIRTHDAY, response.error_code()); | |
| } | |
| TEST_F(LoopbackServerTest, GetUpdateCommand) { | |
| ClientToServerResponse response = | |
| GetUpdatesForType(EntitySpecifics::kBookmarkFieldNumber); | |
| // Expect to see the four top-level folders in this update already. | |
| EXPECT_EQ(4, response.get_updates().entries_size()); | |
| } | |
| TEST_F(LoopbackServerTest, GetUpdateCommandShouldFilterByDataType) { | |
| ClientToServerResponse response = | |
| GetUpdatesForType(EntitySpecifics::kPreferenceFieldNumber); | |
| // Expect bookmark nodes to be ignored. | |
| EXPECT_EQ(0, response.get_updates().entries_size()); | |
| EXPECT_EQ(1, response.get_updates().new_progress_marker_size()); | |
| } | |
| TEST_F(LoopbackServerTest, ClearServerDataCommand) { | |
| ClientToServerMessage msg; | |
| SyncerProtoUtil::SetProtocolVersion(&msg); | |
| msg.set_share("required"); | |
| msg.set_message_contents(ClientToServerMessage::CLEAR_SERVER_DATA); | |
| ClientToServerResponse response; | |
| EXPECT_TRUE(CallPostAndProcessHeaders(lcm_.get(), msg, &response)); | |
| EXPECT_EQ(SyncEnums::SUCCESS, response.error_code()); | |
| EXPECT_TRUE(response.has_clear_server_data()); | |
| } | |
| TEST_F(LoopbackServerTest, CommitCommand) { | |
| CommitVerifySuccess(NewBookmarkEntity(kUrl1, kBookmarkBar)); | |
| } | |
| TEST_F(LoopbackServerTest, CommitFailureNoTag) { | |
| // Non-bookmarks and non-commit only types must have a | |
| // client_tag_hash, which we don't set. | |
| SyncEntity entity; | |
| entity.mutable_specifics()->mutable_preference(); | |
| CommitVerifyFailure(entity); | |
| } | |
| TEST_F(LoopbackServerTest, CommitBookmarkTombstoneSuccess) { | |
| std::string id1 = CommitVerifySuccess(NewBookmarkEntity(kUrl1, kBookmarkBar)); | |
| std::string id2 = CommitVerifySuccess(NewBookmarkEntity(kUrl2, id1)); | |
| std::string id3 = CommitVerifySuccess(NewBookmarkEntity(kUrl3, kBookmarkBar)); | |
| ClientToServerResponse original_response = | |
| GetUpdatesForType(EntitySpecifics::kBookmarkFieldNumber); | |
| std::map<std::string, SyncEntity> original_bookmarks = | |
| ResponseToMap(original_response); | |
| ASSERT_TRUE(original_bookmarks.contains(id1)); | |
| ASSERT_TRUE(original_bookmarks.contains(id2)); | |
| ASSERT_TRUE(original_bookmarks.contains(id3)); | |
| ASSERT_FALSE(original_bookmarks[id1].deleted()); | |
| ASSERT_FALSE(original_bookmarks[id2].deleted()); | |
| ASSERT_FALSE(original_bookmarks[id3].deleted()); | |
| const std::string birthday = original_response.store_birthday(); | |
| ASSERT_EQ(original_response.get_updates().new_progress_marker_size(), 1); | |
| const std::string progress_token = | |
| original_response.get_updates().new_progress_marker(0).token(); | |
| // Because 2 is a child of 1, deleting 1 will also delete 2. | |
| CommitVerifySuccess(DeletedBookmarkEntity(id1, 10)); | |
| // In an incremental update, tombstones are returned for the deleted entities. | |
| // The remaining entity is unchanged, so is not returned. | |
| std::map<std::string, SyncEntity> incremental_update_bookmarks = | |
| ResponseToMap(GetUpdatesForType(EntitySpecifics::kBookmarkFieldNumber, | |
| birthday, progress_token)); | |
| EXPECT_TRUE(incremental_update_bookmarks.contains(id1)); | |
| EXPECT_TRUE(incremental_update_bookmarks.contains(id2)); | |
| EXPECT_FALSE(incremental_update_bookmarks.contains(id3)); | |
| EXPECT_TRUE(incremental_update_bookmarks[id1].deleted()); | |
| EXPECT_TRUE(incremental_update_bookmarks[id2].deleted()); | |
| // In a full (non-incremental) update, no tombstones are returned - the | |
| // entities just aren't there anymore. | |
| std::map<std::string, SyncEntity> full_update_bookmarks = | |
| ResponseToMap(GetUpdatesForType(EntitySpecifics::kBookmarkFieldNumber)); | |
| EXPECT_FALSE(full_update_bookmarks.contains(id1)); | |
| EXPECT_FALSE(full_update_bookmarks.contains(id2)); | |
| EXPECT_TRUE(full_update_bookmarks.contains(id3)); | |
| EXPECT_FALSE(full_update_bookmarks[id3].deleted()); | |
| } | |
| TEST_F(LoopbackServerTest, CommitBookmarkTombstoneFailure) { | |
| std::string id1 = CommitVerifySuccess(NewBookmarkEntity(kUrl1, kBookmarkBar)); | |
| std::string id2 = CommitVerifySuccess(NewBookmarkEntity(kUrl2, "9" + id1)); | |
| // This write is going to fail, the id is supposed to encode the data type as | |
| // as prefix, by adding 9 we're creating a fake data type. | |
| SyncEntity entity = DeletedBookmarkEntity("9" + id1, 1); | |
| CommitVerifyFailure(entity); | |
| std::map<std::string, SyncEntity> bookmarks = | |
| ResponseToMap(GetUpdatesForType(EntitySpecifics::kBookmarkFieldNumber)); | |
| EXPECT_FALSE(bookmarks[id1].deleted()); | |
| // This is the point of this test, making sure the child doesn't get deleted. | |
| EXPECT_FALSE(bookmarks[id2].deleted()); | |
| } | |
| TEST_F(LoopbackServerTest, LoadSavedState) { | |
| std::string id = CommitVerifySuccess(NewBookmarkEntity(kUrl1, kBookmarkBar)); | |
| ClientToServerMessage get_updates_msg; | |
| SyncerProtoUtil::SetProtocolVersion(&get_updates_msg); | |
| get_updates_msg.set_share("required"); | |
| get_updates_msg.set_message_contents(ClientToServerMessage::GET_UPDATES); | |
| get_updates_msg.mutable_get_updates() | |
| ->add_from_progress_marker() | |
| ->set_data_type_id(EntitySpecifics::kBookmarkFieldNumber); | |
| ClientToServerResponse expected_response; | |
| EXPECT_TRUE(CallPostAndProcessHeaders(lcm_.get(), get_updates_msg, | |
| &expected_response)); | |
| EXPECT_EQ(SyncEnums::SUCCESS, expected_response.error_code()); | |
| ASSERT_TRUE(expected_response.has_get_updates()); | |
| ASSERT_TRUE(expected_response.has_store_birthday()); | |
| lcm_.reset(); | |
| task_environment_.RunUntilIdle(); | |
| LoopbackConnectionManager second_user(persistent_file_); | |
| ClientToServerResponse response; | |
| EXPECT_TRUE( | |
| CallPostAndProcessHeaders(&second_user, get_updates_msg, &response)); | |
| EXPECT_EQ(SyncEnums::SUCCESS, response.error_code()); | |
| ASSERT_TRUE(response.has_get_updates()); | |
| // Expect to see the four top-level folders and the newly added bookmark! | |
| EXPECT_EQ(5, response.get_updates().entries_size()); | |
| EXPECT_EQ(1U, ResponseToMap(response).count(id)); | |
| EXPECT_EQ(expected_response.store_birthday(), response.store_birthday()); | |
| } | |
| TEST_F(LoopbackServerTest, CommitCommandUpdate) { | |
| std::string id = CommitVerifySuccess(NewBookmarkEntity(kUrl1, kBookmarkBar)); | |
| EXPECT_EQ(1U, ResponseToMap( | |
| GetUpdatesForType(EntitySpecifics::kBookmarkFieldNumber)) | |
| .count(id)); | |
| CommitVerifySuccess(UpdatedBookmarkEntity(kUrl2, id, "other_bookmarks", 1)); | |
| ClientToServerResponse response = | |
| GetUpdatesForType(EntitySpecifics::kBookmarkFieldNumber); | |
| ASSERT_TRUE(response.has_get_updates()); | |
| // Expect to see no sixth bookmark! | |
| EXPECT_EQ(5, response.get_updates().entries_size()); | |
| EXPECT_EQ(kUrl2, ResponseToMap(response)[id].specifics().bookmark().url()); | |
| } | |
| TEST_F(LoopbackServerTest, CommitBookmarkCreationWithClientTag) { | |
| const std::string kGuid = base::Uuid::GenerateRandomV4().AsLowercaseString(); | |
| const std::string kClientTagHash = | |
| ClientTagHash::FromUnhashed(BOOKMARKS, kGuid).value(); | |
| SyncEntity entity; | |
| entity.mutable_specifics()->mutable_bookmark()->set_url(kUrl1); | |
| entity.set_parent_id_string(kBookmarkBar); | |
| entity.set_id_string(kGuid); | |
| entity.set_client_tag_hash(kClientTagHash); | |
| const std::string id = CommitVerifySuccess(entity); | |
| std::map<std::string, SyncEntity> bookmarks = | |
| ResponseToMap(GetUpdatesForType(EntitySpecifics::kBookmarkFieldNumber)); | |
| EXPECT_EQ(bookmarks[id].client_tag_hash(), kClientTagHash); | |
| } | |
| // Verifies that a bookmark update (non-creation) does not populate the client | |
| // tag of a bookmark, if no client tag was provided upon creation. | |
| TEST_F(LoopbackServerTest, CommitBookmarkUpdateWithClientTag) { | |
| const std::string kGuid = base::Uuid::GenerateRandomV4().AsLowercaseString(); | |
| const std::string kClientTagHash = | |
| ClientTagHash::FromUnhashed(BOOKMARKS, kGuid).value(); | |
| SyncEntity entity; | |
| entity.mutable_specifics()->mutable_bookmark()->set_url(kUrl1); | |
| entity.set_parent_id_string(kBookmarkBar); | |
| entity.set_id_string(kGuid); | |
| const std::string id = CommitVerifySuccess(entity); | |
| std::map<std::string, SyncEntity> bookmarks = | |
| ResponseToMap(GetUpdatesForType(EntitySpecifics::kBookmarkFieldNumber)); | |
| ASSERT_EQ(bookmarks[id].specifics().bookmark().url(), kUrl1); | |
| ASSERT_FALSE(bookmarks[id].has_client_tag_hash()); | |
| // Issue an update, with the client tag being provided for the first time. | |
| entity.set_id_string(id); | |
| entity.set_client_tag_hash(kClientTagHash); | |
| entity.set_version(1); | |
| entity.mutable_specifics()->mutable_bookmark()->set_url(kUrl2); | |
| CommitVerifySuccess(entity); | |
| bookmarks = | |
| ResponseToMap(GetUpdatesForType(EntitySpecifics::kBookmarkFieldNumber)); | |
| ASSERT_EQ(bookmarks[id].specifics().bookmark().url(), kUrl2); | |
| EXPECT_FALSE(bookmarks[id].has_client_tag_hash()); | |
| } | |
| TEST_F(LoopbackServerTest, | |
| ShouldReplaceNonBookmarkServerEntityOnBookmarkCommit) { | |
| // Ensure the permanent bookmark folders (e.g. `32904_bookmark_bar`, which is | |
| // a PersistentPermanentEntity) are created on the server. | |
| GetUpdatesForType(EntitySpecifics::kBookmarkFieldNumber); | |
| // Committing a bookmark update when the existing server entity is not a | |
| // PersistentBookmarkEntity should safely replace the previous entity without | |
| // downcasting it to PersistentBookmarkEntity. | |
| SyncEntity entity; | |
| entity.set_id_string("32904_bookmark_bar"); | |
| entity.set_parent_id_string("32904_google_chrome_bookmarks"); | |
| entity.set_version(1); | |
| entity.mutable_specifics()->mutable_bookmark()->set_url(kUrl1); | |
| EXPECT_EQ("32904_bookmark_bar", CommitVerifySuccess(entity)); | |
| } | |
| } // namespace syncer |