| // Copyright 2018 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "components/sync/engine/commit_contribution_impl.h" | |
| #include <memory> | |
| #include <string> | |
| #include <utility> | |
| #include "base/base64.h" | |
| #include "base/functional/callback_helpers.h" | |
| #include "base/hash/sha1.h" | |
| #include "base/test/mock_callback.h" | |
| #include "components/sync/base/client_tag_hash.h" | |
| #include "components/sync/base/collaboration_id.h" | |
| #include "components/sync/base/data_type.h" | |
| #include "components/sync/base/unique_position.h" | |
| #include "components/sync/protocol/collaboration_metadata.h" | |
| #include "components/sync/protocol/entity_data.h" | |
| #include "components/sync/protocol/entity_specifics.pb.h" | |
| #include "components/sync/protocol/sharing_message_specifics.pb.h" | |
| #include "components/sync/protocol/sync.pb.h" | |
| #include "components/sync/protocol/sync_entity.pb.h" | |
| #include "testing/gtest/include/gtest/gtest.h" | |
| namespace syncer { | |
| namespace { | |
| using sync_pb::CommitResponse; | |
| using sync_pb::EntitySpecifics; | |
| using sync_pb::SharingMessageCommitError; | |
| using sync_pb::SyncEntity; | |
| using testing::IsEmpty; | |
| using testing::Not; | |
| using testing::SizeIs; | |
| constexpr char kValue[] = "value"; | |
| constexpr char kURL[] = "url"; | |
| constexpr char kTitle[] = "title"; | |
| EntitySpecifics GeneratePreferenceSpecifics(const ClientTagHash& tag, | |
| const std::string& value) { | |
| EntitySpecifics specifics; | |
| specifics.mutable_preference()->set_name(tag.value()); | |
| specifics.mutable_preference()->set_value(value); | |
| return specifics; | |
| } | |
| EntitySpecifics GenerateBookmarkSpecifics(const std::string& url, | |
| const std::string& title) { | |
| EntitySpecifics specifics; | |
| specifics.mutable_bookmark()->set_legacy_canonicalized_title(title); | |
| if (url.empty()) { | |
| specifics.mutable_bookmark()->set_type(sync_pb::BookmarkSpecifics::FOLDER); | |
| } else { | |
| specifics.mutable_bookmark()->set_type(sync_pb::BookmarkSpecifics::URL); | |
| specifics.mutable_bookmark()->set_url(url); | |
| } | |
| *specifics.mutable_bookmark()->mutable_unique_position() = | |
| syncer::UniquePosition::FromInt64(10, | |
| syncer::UniquePosition::RandomSuffix()) | |
| .ToProto(); | |
| return specifics; | |
| } | |
| } // namespace | |
| class CommitContributionImplTest : public testing::Test { | |
| public: | |
| CommitContributionImplTest() = default; | |
| const ClientTagHash& tag() const { return tag_; } | |
| std::unique_ptr<EntityData> CreateDefaultPreferenceEntityData() { | |
| auto data = std::make_unique<syncer::EntityData>(); | |
| data->client_tag_hash = tag(); | |
| data->specifics = GeneratePreferenceSpecifics(tag(), kValue); | |
| data->creation_time = base::Time::Now(); | |
| data->modification_time = data->creation_time; | |
| data->name = "Name:"; | |
| return data; | |
| } | |
| private: | |
| const ClientTagHash tag_{ClientTagHash::FromHashed("tag")}; | |
| }; | |
| TEST_F(CommitContributionImplTest, PopulateCommitProtoDefault) { | |
| const int64_t kBaseVersion = 7; | |
| base::Time creation_time = base::Time::UnixEpoch() + base::Days(1); | |
| base::Time modification_time = creation_time + base::Seconds(1); | |
| std::unique_ptr<EntityData> data = CreateDefaultPreferenceEntityData(); | |
| data->creation_time = creation_time; | |
| data->modification_time = modification_time; | |
| CommitRequestData request_data; | |
| request_data.sequence_number = 2; | |
| request_data.base_version = kBaseVersion; | |
| request_data.specifics_hash = base::Base64Encode( | |
| base::SHA1HashString(data->specifics.SerializeAsString())); | |
| request_data.entity = std::move(data); | |
| SyncEntity entity; | |
| CommitContributionImpl::PopulateCommitProto(PREFERENCES, request_data, | |
| &entity); | |
| // Exhaustively verify the populated SyncEntity. | |
| EXPECT_TRUE(entity.id_string().empty()); | |
| EXPECT_EQ(7, entity.version()); | |
| EXPECT_EQ(modification_time.InMillisecondsFSinceUnixEpoch(), entity.mtime()); | |
| EXPECT_EQ(creation_time.InMillisecondsFSinceUnixEpoch(), entity.ctime()); | |
| EXPECT_FALSE(entity.name().empty()); | |
| EXPECT_FALSE(entity.client_tag_hash().empty()); | |
| EXPECT_EQ(tag().value(), entity.specifics().preference().name()); | |
| EXPECT_FALSE(entity.deleted()); | |
| EXPECT_EQ(kValue, entity.specifics().preference().value()); | |
| EXPECT_TRUE(entity.parent_id_string().empty()); | |
| EXPECT_FALSE(entity.unique_position().has_custom_compressed_v1()); | |
| } | |
| TEST_F(CommitContributionImplTest, PopulateCommitProtoTombstone) { | |
| const int64_t kBaseVersion = 7; | |
| base::Time creation_time = base::Time::UnixEpoch() + base::Days(1); | |
| base::Time modification_time = creation_time + base::Seconds(1); | |
| std::unique_ptr<EntityData> data = CreateDefaultPreferenceEntityData(); | |
| data->creation_time = creation_time; | |
| data->modification_time = modification_time; | |
| data->specifics.Clear(); | |
| // Empty specifics means this is a deletion aka tombstone. | |
| ASSERT_TRUE(data->is_deleted()); | |
| CommitRequestData request_data; | |
| request_data.sequence_number = 2; | |
| request_data.base_version = kBaseVersion; | |
| request_data.specifics_hash = base::Base64Encode( | |
| base::SHA1HashString(data->specifics.SerializeAsString())); | |
| request_data.entity = std::move(data); | |
| SyncEntity entity; | |
| CommitContributionImpl::PopulateCommitProto(PREFERENCES, request_data, | |
| &entity); | |
| // Exhaustively verify the populated SyncEntity. | |
| // It's a deletion! | |
| EXPECT_TRUE(entity.deleted()); | |
| // Some "standard" fields are the same as for non-tombstone commits. | |
| EXPECT_TRUE(entity.id_string().empty()); | |
| EXPECT_EQ(7, entity.version()); | |
| EXPECT_FALSE(entity.client_tag_hash().empty()); | |
| EXPECT_TRUE(entity.parent_id_string().empty()); | |
| EXPECT_FALSE(entity.unique_position().has_custom_compressed_v1()); | |
| // The specifics field should be empty. | |
| // Note: AdjustCommitProto() would ensure that the appropriate specifics is | |
| // set (`has_preference()` is true), but this test doesn't execute that code. | |
| EXPECT_EQ(0u, entity.specifics().ByteSizeLong()); | |
| // For deletions, mtime should still be set, but ctime shouldn't. | |
| EXPECT_EQ(modification_time.InMillisecondsFSinceUnixEpoch(), entity.mtime()); | |
| EXPECT_FALSE(entity.has_ctime()); | |
| // The entity name is still passed on, if it was set in the input EntityData | |
| // (which it is in this test, even though in practice it isn't). | |
| EXPECT_FALSE(entity.name().empty()); | |
| } | |
| TEST_F(CommitContributionImplTest, PopulateCommitProtoBookmark) { | |
| const int64_t kBaseVersion = 7; | |
| base::Time creation_time = base::Time::UnixEpoch() + base::Days(1); | |
| base::Time modification_time = creation_time + base::Seconds(1); | |
| auto data = std::make_unique<syncer::EntityData>(); | |
| data->id = "bookmark"; | |
| data->specifics = GenerateBookmarkSpecifics(kURL, kTitle); | |
| // These fields are not really used for much, but we set them anyway | |
| // to make this item look more realistic. | |
| data->creation_time = creation_time; | |
| data->modification_time = modification_time; | |
| data->name = "Name:"; | |
| data->legacy_parent_id = "ParentOf:"; | |
| CommitRequestData request_data; | |
| request_data.sequence_number = 2; | |
| request_data.base_version = kBaseVersion; | |
| request_data.specifics_hash = base::Base64Encode( | |
| base::SHA1HashString(data->specifics.SerializeAsString())); | |
| request_data.deprecated_bookmark_folder = false; | |
| request_data.deprecated_bookmark_unique_position = | |
| UniquePosition::FromProto(data->specifics.bookmark().unique_position()); | |
| request_data.entity = std::move(data); | |
| SyncEntity entity; | |
| CommitContributionImpl::PopulateCommitProto(BOOKMARKS, request_data, &entity); | |
| // Exhaustively verify the populated SyncEntity. | |
| EXPECT_FALSE(entity.id_string().empty()); | |
| EXPECT_EQ(7, entity.version()); | |
| EXPECT_EQ(modification_time.InMillisecondsFSinceUnixEpoch(), entity.mtime()); | |
| EXPECT_EQ(creation_time.InMillisecondsFSinceUnixEpoch(), entity.ctime()); | |
| EXPECT_FALSE(entity.name().empty()); | |
| EXPECT_TRUE(entity.client_tag_hash().empty()); | |
| EXPECT_EQ(kURL, entity.specifics().bookmark().url()); | |
| EXPECT_FALSE(entity.deleted()); | |
| EXPECT_EQ(kTitle, entity.specifics().bookmark().legacy_canonicalized_title()); | |
| EXPECT_FALSE(entity.folder()); | |
| EXPECT_FALSE(entity.parent_id_string().empty()); | |
| EXPECT_TRUE(entity.unique_position().has_custom_compressed_v1()); | |
| } | |
| TEST_F(CommitContributionImplTest, PopulateCommitProtoBookmarkFolder) { | |
| const int64_t kBaseVersion = 7; | |
| base::Time creation_time = base::Time::UnixEpoch() + base::Days(1); | |
| base::Time modification_time = creation_time + base::Seconds(1); | |
| auto data = std::make_unique<syncer::EntityData>(); | |
| data->id = "bookmark"; | |
| data->specifics = GenerateBookmarkSpecifics(/*url=*/"", kTitle); | |
| // These fields are not really used for much, but we set them anyway | |
| // to make this item look more realistic. | |
| data->creation_time = creation_time; | |
| data->modification_time = modification_time; | |
| data->name = "Name:"; | |
| data->legacy_parent_id = "ParentOf:"; | |
| CommitRequestData request_data; | |
| request_data.sequence_number = 2; | |
| request_data.base_version = kBaseVersion; | |
| request_data.specifics_hash = base::Base64Encode( | |
| base::SHA1HashString(data->specifics.SerializeAsString())); | |
| request_data.deprecated_bookmark_folder = true; | |
| request_data.deprecated_bookmark_unique_position = | |
| UniquePosition::FromProto(data->specifics.bookmark().unique_position()); | |
| request_data.entity = std::move(data); | |
| SyncEntity entity; | |
| CommitContributionImpl::PopulateCommitProto(BOOKMARKS, request_data, &entity); | |
| // Exhaustively verify the populated SyncEntity. | |
| EXPECT_FALSE(entity.id_string().empty()); | |
| EXPECT_EQ(7, entity.version()); | |
| EXPECT_EQ(modification_time.InMillisecondsFSinceUnixEpoch(), entity.mtime()); | |
| EXPECT_EQ(creation_time.InMillisecondsFSinceUnixEpoch(), entity.ctime()); | |
| EXPECT_FALSE(entity.name().empty()); | |
| EXPECT_TRUE(entity.client_tag_hash().empty()); | |
| EXPECT_FALSE(entity.specifics().bookmark().has_url()); | |
| EXPECT_FALSE(entity.deleted()); | |
| EXPECT_EQ(kTitle, entity.specifics().bookmark().legacy_canonicalized_title()); | |
| EXPECT_TRUE(entity.folder()); | |
| EXPECT_FALSE(entity.parent_id_string().empty()); | |
| EXPECT_TRUE(entity.unique_position().has_custom_compressed_v1()); | |
| } | |
| TEST_F(CommitContributionImplTest, ShouldPropagateFailedItemsOnCommitResponse) { | |
| auto data = std::make_unique<syncer::EntityData>(); | |
| data->client_tag_hash = ClientTagHash::FromHashed("hash"); | |
| auto request_data = std::make_unique<CommitRequestData>(); | |
| request_data->entity = std::move(data); | |
| CommitRequestDataList requests_data; | |
| requests_data.push_back(std::move(request_data)); | |
| FailedCommitResponseDataList actual_error_response_list; | |
| auto on_commit_response_callback = base::BindOnce( | |
| [](FailedCommitResponseDataList* actual_error_response_list, | |
| const CommitResponseDataList& committed_response_list, | |
| const FailedCommitResponseDataList& error_response_list) { | |
| // We put expectations outside of the callback, so that they fail if | |
| // callback is not ran. | |
| *actual_error_response_list = error_response_list; | |
| }, | |
| &actual_error_response_list); | |
| CommitContributionImpl contribution( | |
| PASSWORDS, sync_pb::DataTypeContext(), std::move(requests_data), | |
| std::move(on_commit_response_callback), | |
| /*on_full_commit_failure_callback=*/base::NullCallback(), | |
| PassphraseType::kCustomPassphrase); | |
| sync_pb::ClientToServerMessage msg; | |
| contribution.AddToCommitMessage(&msg); | |
| sync_pb::ClientToServerResponse response; | |
| sync_pb::CommitResponse* commit_response = response.mutable_commit(); | |
| { | |
| sync_pb::CommitResponse_EntryResponse* entry = | |
| commit_response->add_entryresponse(); | |
| entry->set_response_type(CommitResponse::TRANSIENT_ERROR); | |
| SharingMessageCommitError* sharing_message_error = | |
| entry->mutable_datatype_specific_error() | |
| ->mutable_sharing_message_error(); | |
| sharing_message_error->set_error_code( | |
| SharingMessageCommitError::INVALID_ARGUMENT); | |
| } | |
| StatusController status; | |
| contribution.ProcessCommitResponse(response, &status); | |
| ASSERT_EQ(1u, actual_error_response_list.size()); | |
| FailedCommitResponseData failed_item = actual_error_response_list[0]; | |
| EXPECT_EQ(ClientTagHash::FromHashed("hash"), failed_item.client_tag_hash); | |
| EXPECT_EQ(CommitResponse::TRANSIENT_ERROR, failed_item.response_type); | |
| EXPECT_EQ( | |
| SharingMessageCommitError::INVALID_ARGUMENT, | |
| failed_item.datatype_specific_error.sharing_message_error().error_code()); | |
| } | |
| TEST_F(CommitContributionImplTest, ShouldPropagateFullCommitFailure) { | |
| base::MockOnceCallback<void(SyncCommitError commit_error)> | |
| on_commit_failure_callback; | |
| EXPECT_CALL(on_commit_failure_callback, Run(SyncCommitError::kNetworkError)); | |
| CommitContributionImpl contribution( | |
| BOOKMARKS, sync_pb::DataTypeContext(), CommitRequestDataList(), | |
| /*on_commit_response_callback=*/base::NullCallback(), | |
| on_commit_failure_callback.Get(), PassphraseType::kKeystorePassphrase); | |
| contribution.ProcessCommitFailure(SyncCommitError::kNetworkError); | |
| } | |
| TEST_F(CommitContributionImplTest, ShouldPopulateIdStringForCommitOnlyTypes) { | |
| // Create non-empty commit-only entity. | |
| auto data = std::make_unique<syncer::EntityData>(); | |
| data->client_tag_hash = ClientTagHash::FromHashed("hash"); | |
| data->specifics.mutable_sharing_message()->set_message_id("message_id"); | |
| auto request_data = std::make_unique<CommitRequestData>(); | |
| request_data->entity = std::move(data); | |
| request_data->base_version = kUncommittedVersion; | |
| CommitRequestDataList requests_data; | |
| requests_data.push_back(std::move(request_data)); | |
| CommitContributionImpl contribution( | |
| SHARING_MESSAGE, sync_pb::DataTypeContext(), std::move(requests_data), | |
| /*on_commit_response_callback=*/base::NullCallback(), | |
| /*on_full_commit_failure_callback=*/base::NullCallback(), | |
| PassphraseType::kKeystorePassphrase); | |
| sync_pb::ClientToServerMessage msg; | |
| contribution.AddToCommitMessage(&msg); | |
| ASSERT_THAT(msg.commit().entries(), SizeIs(1)); | |
| EXPECT_THAT(msg.commit().entries(0).id_string(), Not(IsEmpty())); | |
| } | |
| TEST_F(CommitContributionImplTest, ShouldPopulateCollaborationId) { | |
| std::unique_ptr<EntityData> data = CreateDefaultPreferenceEntityData(); | |
| data->collaboration_metadata = CollaborationMetadata::ForLocalChange( | |
| /*changed_by=*/GaiaId(), CollaborationId("collaboration")); | |
| CommitRequestData request_data; | |
| request_data.sequence_number = 2; | |
| request_data.base_version = 123; | |
| request_data.specifics_hash = base::Base64Encode( | |
| base::SHA1HashString(data->specifics.SerializeAsString())); | |
| request_data.entity = std::move(data); | |
| SyncEntity entity; | |
| CommitContributionImpl::PopulateCommitProto(PREFERENCES, request_data, | |
| &entity); | |
| EXPECT_EQ(entity.collaboration().collaboration_id(), "collaboration"); | |
| } | |
| } // namespace syncer |