| // Copyright 2018 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 "components/sync/engine_impl/non_blocking_type_commit_contribution.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/base64.h" |
| #include "base/bind_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/model_type.h" |
| #include "components/sync/base/unique_position.h" |
| #include "components/sync/nigori/cryptographer_impl.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; |
| |
| const ClientTagHash kTag = ClientTagHash::FromHashed("tag"); |
| const char kValue[] = "value"; |
| const char kURL[] = "url"; |
| const 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_url(url); |
| specifics.mutable_bookmark()->set_legacy_canonicalized_title(title); |
| return specifics; |
| } |
| |
| TEST(NonBlockingTypeCommitContributionTest, PopulateCommitProtoDefault) { |
| const int64_t kBaseVersion = 7; |
| base::Time creation_time = |
| base::Time::UnixEpoch() + base::TimeDelta::FromDays(1); |
| base::Time modification_time = |
| creation_time + base::TimeDelta::FromSeconds(1); |
| |
| auto data = std::make_unique<syncer::EntityData>(); |
| |
| data->client_tag_hash = kTag; |
| data->specifics = GeneratePreferenceSpecifics(kTag, kValue); |
| |
| // 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:"; |
| |
| CommitRequestData request_data; |
| request_data.sequence_number = 2; |
| request_data.base_version = kBaseVersion; |
| base::Base64Encode(base::SHA1HashString(data->specifics.SerializeAsString()), |
| &request_data.specifics_hash); |
| request_data.entity = std::move(data); |
| |
| SyncEntity entity; |
| NonBlockingTypeCommitContribution::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.ToJsTime(), entity.mtime()); |
| EXPECT_EQ(creation_time.ToJsTime(), entity.ctime()); |
| EXPECT_FALSE(entity.name().empty()); |
| EXPECT_FALSE(entity.client_defined_unique_tag().empty()); |
| EXPECT_EQ(kTag.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()); |
| EXPECT_EQ(0, entity.position_in_parent()); |
| } |
| |
| TEST(NonBlockingTypeCommitContributionTest, PopulateCommitProtoBookmark) { |
| const int64_t kBaseVersion = 7; |
| base::Time creation_time = |
| base::Time::UnixEpoch() + base::TimeDelta::FromDays(1); |
| base::Time modification_time = |
| creation_time + base::TimeDelta::FromSeconds(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->parent_id = "ParentOf:"; |
| data->is_folder = true; |
| syncer::UniquePosition uniquePosition = syncer::UniquePosition::FromInt64( |
| 10, syncer::UniquePosition::RandomSuffix()); |
| data->unique_position = uniquePosition.ToProto(); |
| |
| CommitRequestData request_data; |
| request_data.sequence_number = 2; |
| request_data.base_version = kBaseVersion; |
| base::Base64Encode(base::SHA1HashString(data->specifics.SerializeAsString()), |
| &request_data.specifics_hash); |
| request_data.entity = std::move(data); |
| |
| SyncEntity entity; |
| NonBlockingTypeCommitContribution::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.ToJsTime(), entity.mtime()); |
| EXPECT_EQ(creation_time.ToJsTime(), entity.ctime()); |
| EXPECT_FALSE(entity.name().empty()); |
| EXPECT_TRUE(entity.client_defined_unique_tag().empty()); |
| EXPECT_EQ(kURL, entity.specifics().bookmark().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()); |
| EXPECT_NE(0, entity.position_in_parent()); |
| } |
| |
| // Verifies how PASSWORDS protos are committed on the wire, making sure the data |
| // is properly encrypted except for password metadata. |
| TEST(NonBlockingTypeCommitContributionTest, |
| PopulateCommitProtoPasswordWithoutCustomPassphrase) { |
| const std::string kMetadataUrl = "http://foo.com"; |
| const std::string kSignonRealm = "signon_realm"; |
| const int64_t kBaseVersion = 7; |
| |
| auto data = std::make_unique<syncer::EntityData>(); |
| data->client_tag_hash = kTag; |
| sync_pb::PasswordSpecificsData* password_data = |
| data->specifics.mutable_password()->mutable_client_only_encrypted_data(); |
| password_data->set_signon_realm(kSignonRealm); |
| |
| data->specifics.mutable_password()->mutable_unencrypted_metadata()->set_url( |
| kMetadataUrl); |
| |
| auto request_data = std::make_unique<CommitRequestData>(); |
| request_data->sequence_number = 2; |
| request_data->base_version = kBaseVersion; |
| base::Base64Encode(base::SHA1HashString(data->specifics.SerializeAsString()), |
| &request_data->specifics_hash); |
| request_data->entity = std::move(data); |
| |
| DataTypeDebugInfoEmitter debug_info_emitter(PASSWORDS); |
| |
| std::unique_ptr<CryptographerImpl> cryptographer = |
| CryptographerImpl::FromSingleKeyForTesting("dummy"); |
| |
| CommitRequestDataList requests_data; |
| requests_data.push_back(std::move(request_data)); |
| NonBlockingTypeCommitContribution contribution( |
| PASSWORDS, sync_pb::DataTypeContext(), std::move(requests_data), |
| /*on_commit_response_callback=*/base::NullCallback(), |
| /*on_full_commit_failure_callback=*/base::NullCallback(), |
| cryptographer.get(), PassphraseType::kImplicitPassphrase, |
| &debug_info_emitter, |
| /*only_commit_specifics=*/false); |
| |
| sync_pb::ClientToServerMessage msg; |
| contribution.AddToCommitMessage(&msg); |
| contribution.CleanUp(); |
| |
| ASSERT_EQ(1, msg.commit().entries().size()); |
| SyncEntity entity = msg.commit().entries(0); |
| |
| // Exhaustively verify the populated SyncEntity. |
| EXPECT_TRUE(entity.id_string().empty()); |
| EXPECT_EQ(7, entity.version()); |
| EXPECT_EQ("encrypted", entity.name()); |
| EXPECT_EQ(kTag.value(), entity.client_defined_unique_tag()); |
| EXPECT_FALSE(entity.deleted()); |
| EXPECT_FALSE(entity.specifics().has_encrypted()); |
| EXPECT_TRUE(entity.specifics().has_password()); |
| EXPECT_EQ(kSignonRealm, |
| entity.specifics().password().unencrypted_metadata().url()); |
| EXPECT_TRUE( |
| entity.specifics().password().unencrypted_metadata().has_blacklisted()); |
| EXPECT_FALSE( |
| entity.specifics().password().unencrypted_metadata().blacklisted()); |
| EXPECT_FALSE(entity.specifics().password().encrypted().blob().empty()); |
| EXPECT_TRUE(entity.parent_id_string().empty()); |
| EXPECT_FALSE(entity.unique_position().has_custom_compressed_v1()); |
| EXPECT_EQ(0, entity.position_in_parent()); |
| } |
| |
| // Same as above but uses CUSTOM_PASSPHRASE. In this case, field |
| // |unencrypted_metadata| should be cleared. |
| TEST(NonBlockingTypeCommitContributionTest, |
| PopulateCommitProtoPasswordWithCustomPassphrase) { |
| const std::string kMetadataUrl = "http://foo.com"; |
| const std::string kSignonRealm = "signon_realm"; |
| const int64_t kBaseVersion = 7; |
| |
| auto data = std::make_unique<syncer::EntityData>(); |
| data->client_tag_hash = kTag; |
| sync_pb::PasswordSpecificsData* password_data = |
| data->specifics.mutable_password()->mutable_client_only_encrypted_data(); |
| password_data->set_signon_realm(kSignonRealm); |
| |
| data->specifics.mutable_password()->mutable_unencrypted_metadata()->set_url( |
| kMetadataUrl); |
| |
| auto request_data = std::make_unique<CommitRequestData>(); |
| request_data->sequence_number = 2; |
| request_data->base_version = kBaseVersion; |
| base::Base64Encode(base::SHA1HashString(data->specifics.SerializeAsString()), |
| &request_data->specifics_hash); |
| request_data->entity = std::move(data); |
| |
| DataTypeDebugInfoEmitter debug_info_emitter(PASSWORDS); |
| |
| std::unique_ptr<CryptographerImpl> cryptographer = |
| CryptographerImpl::FromSingleKeyForTesting("dummy"); |
| |
| CommitRequestDataList requests_data; |
| requests_data.push_back(std::move(request_data)); |
| NonBlockingTypeCommitContribution contribution( |
| PASSWORDS, sync_pb::DataTypeContext(), std::move(requests_data), |
| /*on_commit_response_callback=*/base::NullCallback(), |
| /*on_full_commit_failure_callback=*/base::NullCallback(), |
| cryptographer.get(), PassphraseType::kCustomPassphrase, |
| &debug_info_emitter, |
| /*only_commit_specifics=*/false); |
| |
| sync_pb::ClientToServerMessage msg; |
| contribution.AddToCommitMessage(&msg); |
| contribution.CleanUp(); |
| |
| ASSERT_EQ(1, msg.commit().entries().size()); |
| SyncEntity entity = msg.commit().entries(0); |
| |
| // Exhaustively verify the populated SyncEntity. |
| EXPECT_TRUE(entity.id_string().empty()); |
| EXPECT_EQ(7, entity.version()); |
| EXPECT_EQ("encrypted", entity.name()); |
| EXPECT_EQ(kTag.value(), entity.client_defined_unique_tag()); |
| EXPECT_FALSE(entity.deleted()); |
| EXPECT_FALSE(entity.specifics().has_encrypted()); |
| EXPECT_TRUE(entity.specifics().has_password()); |
| EXPECT_FALSE(entity.specifics().password().encrypted().blob().empty()); |
| EXPECT_FALSE(entity.specifics().password().has_unencrypted_metadata()); |
| EXPECT_TRUE(entity.parent_id_string().empty()); |
| EXPECT_FALSE(entity.unique_position().has_custom_compressed_v1()); |
| EXPECT_EQ(0, entity.position_in_parent()); |
| } |
| |
| TEST(NonBlockingTypeCommitContributionTest, |
| 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)); |
| |
| DataTypeDebugInfoEmitter debug_info_emitter(PASSWORDS); |
| |
| std::unique_ptr<CryptographerImpl> cryptographer = |
| CryptographerImpl::CreateEmpty(); |
| |
| 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); |
| |
| NonBlockingTypeCommitContribution contribution( |
| PASSWORDS, sync_pb::DataTypeContext(), std::move(requests_data), |
| std::move(on_commit_response_callback), |
| /*on_full_commit_failure_callback=*/base::NullCallback(), |
| cryptographer.get(), PassphraseType::kCustomPassphrase, |
| &debug_info_emitter, |
| /*only_commit_specifics=*/false); |
| |
| 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); |
| contribution.CleanUp(); |
| |
| 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(NonBlockingTypeCommitContributionTest, ShouldPropagateFullCommitFailure) { |
| DataTypeDebugInfoEmitter debug_info_emitter(BOOKMARKS); |
| |
| base::MockOnceCallback<void(SyncCommitError commit_error)> |
| on_commit_failure_callback; |
| EXPECT_CALL(on_commit_failure_callback, Run(SyncCommitError::kNetworkError)); |
| |
| NonBlockingTypeCommitContribution contribution( |
| BOOKMARKS, sync_pb::DataTypeContext(), CommitRequestDataList(), |
| /*on_commit_response_callback=*/base::NullCallback(), |
| on_commit_failure_callback.Get(), /*cryptographer=*/nullptr, |
| PassphraseType::kKeystorePassphrase, &debug_info_emitter, |
| /*only_commit_specifics=*/false); |
| |
| contribution.ProcessCommitFailure(SyncCommitError::kNetworkError); |
| contribution.CleanUp(); |
| } |
| |
| } // namespace |
| |
| } // namespace syncer |