| // 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 "components/sync/engine/model_type_worker.h" |
| |
| #include <stdint.h> |
| |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/guid.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/threading/thread.h" |
| #include "base/time/time.h" |
| #include "components/sync/base/client_tag_hash.h" |
| #include "components/sync/base/features.h" |
| #include "components/sync/base/unique_position.h" |
| #include "components/sync/engine/cancelation_signal.h" |
| #include "components/sync/engine/commit_contribution.h" |
| #include "components/sync/engine/cycle/entity_change_metric_recording.h" |
| #include "components/sync/engine/cycle/status_controller.h" |
| #include "components/sync/engine/model_type_processor.h" |
| #include "components/sync/protocol/autofill_specifics.pb.h" |
| #include "components/sync/protocol/entity_specifics.pb.h" |
| #include "components/sync/protocol/model_type_state.pb.h" |
| #include "components/sync/protocol/password_specifics.pb.h" |
| #include "components/sync/protocol/sync.pb.h" |
| #include "components/sync/protocol/sync_entity.pb.h" |
| #include "components/sync/test/engine/fake_cryptographer.h" |
| #include "components/sync/test/engine/mock_model_type_processor.h" |
| #include "components/sync/test/engine/mock_nudge_handler.h" |
| #include "components/sync/test/engine/single_type_mock_server.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using base::Time; |
| using sync_pb::EntitySpecifics; |
| using sync_pb::ModelTypeState; |
| using sync_pb::SyncEntity; |
| using testing::IsNull; |
| using testing::NotNull; |
| |
| namespace syncer { |
| |
| namespace { |
| |
| const char kEncryptionKeyNamePrefix[] = "key"; |
| const char kTag1[] = "tag1"; |
| const char kTag2[] = "tag2"; |
| const char kTag3[] = "tag3"; |
| const char kValue1[] = "value1"; |
| const char kValue2[] = "value2"; |
| const char kValue3[] = "value3"; |
| |
| EntitySpecifics GenerateSpecifics(const std::string& tag, |
| const std::string& value) { |
| EntitySpecifics specifics; |
| specifics.mutable_preference()->set_name(tag); |
| specifics.mutable_preference()->set_value(value); |
| return specifics; |
| } |
| |
| std::string GetNthKeyName(int n) { |
| return kEncryptionKeyNamePrefix + base::NumberToString(n); |
| } |
| |
| sync_pb::EntitySpecifics EncryptPasswordSpecificsWithNthKey( |
| int n, |
| const sync_pb::PasswordSpecificsData& unencrypted_password) { |
| sync_pb::EntitySpecifics encrypted_specifics; |
| FakeCryptographer::FromSingleDefaultKey(GetNthKeyName(n)) |
| ->EncryptString( |
| unencrypted_password.SerializeAsString(), |
| encrypted_specifics.mutable_password()->mutable_encrypted()); |
| return encrypted_specifics; |
| } |
| |
| } // namespace |
| |
| // Tests the ModelTypeWorker. |
| // |
| // This class passes messages between the model thread and sync server. |
| // As such, its code is subject to lots of different race conditions. This |
| // test harness lets us exhaustively test all possible races. We try to |
| // focus on just a few interesting cases. |
| // |
| // Inputs: |
| // - Initial data type state from the model thread. |
| // - Commit requests from the model thread. |
| // - Update responses from the server. |
| // - Commit responses from the server. |
| // - The cryptographer, if encryption is enabled. |
| // |
| // Outputs: |
| // - Commit requests to the server. |
| // - Commit responses to the model thread. |
| // - Update responses to the model thread. |
| // - Nudges to the sync scheduler. |
| // |
| // We use the MockModelTypeProcessor to stub out all communication |
| // with the model thread. That interface is synchronous, which makes it |
| // much easier to test races. |
| // |
| // The interface with the server is built around "pulling" data from this |
| // class, so we don't have to mock out any of it. We wrap it with some |
| // convenience functions so we can emulate server behavior. |
| class ModelTypeWorkerTest : public ::testing::Test { |
| protected: |
| static ClientTagHash GenerateTagHash(const std::string& tag) { |
| if (tag.empty()) { |
| return ClientTagHash(); |
| } |
| return ClientTagHash::FromUnhashed(PREFERENCES, tag); |
| } |
| |
| const ClientTagHash kHash1 = GenerateTagHash(kTag1); |
| const ClientTagHash kHash2 = GenerateTagHash(kTag2); |
| const ClientTagHash kHash3 = GenerateTagHash(kTag3); |
| |
| ModelTypeWorkerTest() |
| : ModelTypeWorkerTest(PREFERENCES, /*is_encrypted_type=*/false) {} |
| |
| ModelTypeWorkerTest(ModelType model_type, bool is_encrypted_type) |
| : model_type_(model_type), |
| is_encrypted_type_(is_encrypted_type), |
| mock_server_(std::make_unique<SingleTypeMockServer>(model_type)) {} |
| |
| ~ModelTypeWorkerTest() override = default; |
| |
| // One of these Initialize functions should be called at the beginning of |
| // each test. |
| |
| // Initializes with no data type state. We will be unable to perform any |
| // significant server action until we receive an update response that |
| // contains the type root node for this type. |
| void FirstInitialize() { |
| ModelTypeState initial_state; |
| initial_state.mutable_progress_marker()->set_data_type_id( |
| GetSpecificsFieldNumberFromModelType(model_type_)); |
| |
| InitializeWithState(model_type_, initial_state); |
| } |
| |
| // Initializes with some existing data type state. Allows us to start |
| // committing items right away. |
| void NormalInitialize() { |
| ModelTypeState initial_state; |
| initial_state.mutable_progress_marker()->set_data_type_id( |
| GetSpecificsFieldNumberFromModelType(model_type_)); |
| initial_state.mutable_progress_marker()->set_token( |
| "some_saved_progress_token"); |
| |
| initial_state.set_initial_sync_done(true); |
| |
| InitializeWithState(model_type_, initial_state); |
| |
| nudge_handler()->ClearCounters(); |
| } |
| |
| void InitializeCommitOnly(ModelType model_type) { |
| mock_server_ = std::make_unique<SingleTypeMockServer>(model_type); |
| |
| // Don't set progress marker, commit only types don't use them. |
| ModelTypeState initial_state; |
| initial_state.set_initial_sync_done(true); |
| |
| InitializeWithState(USER_EVENTS, initial_state); |
| } |
| |
| // Initialize with a custom initial ModelTypeState and pending updates. |
| void InitializeWithState(const ModelType type, const ModelTypeState& state) { |
| DCHECK(!worker_); |
| worker_ = std::make_unique<ModelTypeWorker>( |
| type, state, &cryptographer_, is_encrypted_type_, |
| PassphraseType::kImplicitPassphrase, &mock_nudge_handler_, |
| &cancelation_signal_); |
| |
| // We don't get to own this object. The |worker_| keeps a unique_ptr to it. |
| auto processor = std::make_unique<MockModelTypeProcessor>(); |
| mock_type_processor_ = processor.get(); |
| processor->SetDisconnectCallback(base::BindOnce( |
| &ModelTypeWorkerTest::DisconnectProcessor, base::Unretained(this))); |
| worker_->ConnectSync(std::move(processor)); |
| } |
| |
| // Mimic a Nigori update with a keybag that cannot be decrypted, which means |
| // the cryptographer becomes unusable (no default key until the issue gets |
| // resolved, via DecryptPendingKey()). |
| void AddPendingKey() { |
| AddPendingKeyWithoutEnablingEncryption(); |
| if (!is_encrypted_type_ && worker()) { |
| worker()->EnableEncryption(); |
| } |
| is_encrypted_type_ = true; |
| } |
| |
| void AddPendingKeyWithoutEnablingEncryption() { |
| DCHECK(encryption_keys_count_ == 0 || |
| cryptographer_.GetDefaultEncryptionKeyName() == |
| GetNthKeyName(encryption_keys_count_)); |
| encryption_keys_count_++; |
| cryptographer_.ClearDefaultEncryptionKey(); |
| if (worker()) { |
| worker()->OnCryptographerChange(); |
| } |
| } |
| |
| // Must only be called if there was a previous call to AddPendingKey(). |
| // Decrypts the pending key and adds it to the cryptographer. |
| void DecryptPendingKey() { |
| DCHECK_GT(encryption_keys_count_, 0); |
| DCHECK(cryptographer_.GetDefaultEncryptionKeyName().empty()); |
| std::string last_key_name = GetNthKeyName(encryption_keys_count_); |
| cryptographer_.AddEncryptionKey(last_key_name); |
| cryptographer_.SelectDefaultEncryptionKey(last_key_name); |
| |
| if (worker()) { |
| worker()->OnCryptographerChange(); |
| } |
| } |
| |
| // Modifies the input/output parameter |specifics| by encrypting it with |
| // the n-th encryption key. |
| void EncryptUpdateWithNthKey(int n, EntitySpecifics* specifics) { |
| EntitySpecifics original_specifics = *specifics; |
| std::string plaintext; |
| original_specifics.SerializeToString(&plaintext); |
| |
| specifics->Clear(); |
| AddDefaultFieldValue(model_type_, specifics); |
| FakeCryptographer::FromSingleDefaultKey(GetNthKeyName(n)) |
| ->EncryptString(plaintext, specifics->mutable_encrypted()); |
| } |
| |
| // Use the Nth nigori instance to encrypt incoming updates. |
| // The default value, zero, indicates no encryption. |
| void SetUpdateEncryptionFilter(int n) { update_encryption_filter_index_ = n; } |
| |
| // Modifications on the model thread that get sent to the worker under test. |
| |
| CommitRequestDataList GenerateCommitRequest(const std::string& name, |
| const std::string& value) { |
| return GenerateCommitRequest(GenerateTagHash(name), |
| GenerateSpecifics(name, value)); |
| } |
| |
| CommitRequestDataList GenerateCommitRequest( |
| const ClientTagHash& tag_hash, |
| const EntitySpecifics& specifics) { |
| CommitRequestDataList commit_request; |
| commit_request.push_back(processor()->CommitRequest(tag_hash, specifics)); |
| return commit_request; |
| } |
| |
| CommitRequestDataList GenerateDeleteRequest(const std::string& tag) { |
| CommitRequestDataList request; |
| const ClientTagHash tag_hash = GenerateTagHash(tag); |
| request.push_back(processor()->DeleteRequest(tag_hash)); |
| return request; |
| } |
| |
| // Pretend to receive update messages from the server. |
| |
| void TriggerTypeRootUpdateFromServer() { |
| SyncEntity entity = server()->TypeRootUpdate(); |
| worker()->ProcessGetUpdatesResponse(server()->GetProgress(), |
| server()->GetContext(), {&entity}, |
| &status_controller_); |
| worker()->ApplyUpdates(&status_controller_); |
| } |
| |
| void TriggerPartialUpdateFromServer(int64_t version_offset, |
| const std::string& tag, |
| const std::string& value) { |
| SyncEntity entity = server()->UpdateFromServer( |
| version_offset, GenerateTagHash(tag), GenerateSpecifics(tag, value)); |
| |
| if (update_encryption_filter_index_ != 0) { |
| EncryptUpdateWithNthKey(update_encryption_filter_index_, |
| entity.mutable_specifics()); |
| } |
| |
| worker()->ProcessGetUpdatesResponse(server()->GetProgress(), |
| server()->GetContext(), {&entity}, |
| &status_controller_); |
| } |
| |
| void TriggerPartialUpdateFromServer(int64_t version_offset, |
| const std::string& tag1, |
| const std::string& value1, |
| const std::string& tag2, |
| const std::string& value2) { |
| SyncEntity entity1 = server()->UpdateFromServer( |
| version_offset, GenerateTagHash(tag1), GenerateSpecifics(tag1, value1)); |
| SyncEntity entity2 = server()->UpdateFromServer( |
| version_offset, GenerateTagHash(tag2), GenerateSpecifics(tag2, value2)); |
| |
| if (update_encryption_filter_index_ != 0) { |
| EncryptUpdateWithNthKey(update_encryption_filter_index_, |
| entity1.mutable_specifics()); |
| EncryptUpdateWithNthKey(update_encryption_filter_index_, |
| entity2.mutable_specifics()); |
| } |
| |
| worker()->ProcessGetUpdatesResponse( |
| server()->GetProgress(), server()->GetContext(), {&entity1, &entity2}, |
| &status_controller_); |
| } |
| |
| void TriggerUpdateFromServer(int64_t version_offset, |
| const std::string& tag, |
| const std::string& value) { |
| TriggerPartialUpdateFromServer(version_offset, tag, value); |
| worker()->ApplyUpdates(&status_controller_); |
| } |
| |
| void TriggerTombstoneFromServer(int64_t version_offset, |
| const std::string& tag) { |
| SyncEntity entity = |
| server()->TombstoneFromServer(version_offset, GenerateTagHash(tag)); |
| |
| if (update_encryption_filter_index_ != 0) { |
| EncryptUpdateWithNthKey(update_encryption_filter_index_, |
| entity.mutable_specifics()); |
| } |
| |
| worker()->ProcessGetUpdatesResponse(server()->GetProgress(), |
| server()->GetContext(), {&entity}, |
| &status_controller_); |
| worker()->ApplyUpdates(&status_controller_); |
| } |
| |
| // Simulates the end of a GU sync cycle and tells the worker to flush changes |
| // to the processor. |
| void ApplyUpdates() { worker()->ApplyUpdates(&status_controller_); } |
| |
| // Delivers specified protos as updates. |
| // |
| // Does not update mock server state. Should be used as a last resort when |
| // writing test cases that require entities that don't fit the normal sync |
| // protocol. Try to use the other, higher level methods if possible. |
| void DeliverRawUpdates(const SyncEntityList& list) { |
| worker()->ProcessGetUpdatesResponse(server()->GetProgress(), |
| server()->GetContext(), list, |
| &status_controller_); |
| worker()->ApplyUpdates(&status_controller_); |
| } |
| |
| // By default, this harness behaves as if all tasks posted to the model |
| // thread are executed immediately. However, this is not necessarily true. |
| // The model's TaskRunner has a queue, and the tasks we post to it could |
| // linger there for a while. In the meantime, the model thread could |
| // continue posting tasks to the worker based on its stale state. |
| // |
| // If you want to test those race cases, then these functions are for you. |
| void SetModelThreadIsSynchronous(bool is_synchronous) { |
| processor()->SetSynchronousExecution(is_synchronous); |
| } |
| void PumpModelThread() { processor()->RunQueuedTasks(); } |
| |
| // Returns true if the |worker_| is ready to commit something. |
| bool WillCommit() { return worker()->GetContribution(INT_MAX) != nullptr; } |
| |
| // Pretend to successfully commit all outstanding unsynced items. |
| // It is safe to call this only if WillCommit() returns true. |
| // Conveniently, this is all one big synchronous operation. The sync thread |
| // remains blocked while the commit is in progress, so we don't need to worry |
| // about other tasks being run between the time when the commit request is |
| // issued and the time when the commit response is received. |
| void DoSuccessfulCommit() { |
| DoSuccessfulCommit(worker()->GetContribution(INT_MAX)); |
| } |
| |
| void DoSuccessfulCommit(std::unique_ptr<CommitContribution> contribution) { |
| DCHECK(contribution); |
| |
| sync_pb::ClientToServerMessage message; |
| contribution->AddToCommitMessage(&message); |
| |
| sync_pb::ClientToServerResponse response = |
| server()->DoSuccessfulCommit(message); |
| |
| contribution->ProcessCommitResponse(response, &status_controller_); |
| } |
| |
| void DoCommitFailure() { |
| std::unique_ptr<CommitContribution> contribution( |
| worker()->GetContribution(INT_MAX)); |
| DCHECK(contribution); |
| |
| contribution->ProcessCommitFailure(SyncCommitError::kNetworkError); |
| } |
| |
| // Callback when processor got disconnected with sync. |
| void DisconnectProcessor() { |
| DCHECK(!is_processor_disconnected_); |
| is_processor_disconnected_ = true; |
| } |
| |
| bool IsProcessorDisconnected() { return is_processor_disconnected_; } |
| |
| void ResetWorker() { worker_.reset(); } |
| |
| MockModelTypeProcessor* processor() { return mock_type_processor_; } |
| ModelTypeWorker* worker() { return worker_.get(); } |
| SingleTypeMockServer* server() { return mock_server_.get(); } |
| MockNudgeHandler* nudge_handler() { return &mock_nudge_handler_; } |
| StatusController* status_controller() { return &status_controller_; } |
| std::string default_encryption_key_name() { |
| return cryptographer_.GetDefaultEncryptionKeyName(); |
| } |
| |
| private: |
| base::test::SingleThreadTaskEnvironment task_environment_; |
| |
| const ModelType model_type_; |
| |
| FakeCryptographer cryptographer_; |
| |
| // Determines whether |worker_| has access to the cryptographer or not. |
| bool is_encrypted_type_ = false; |
| |
| // The number of encryption keys known to the cryptographer. Keys are |
| // identified by an index from 1 to |encryption_keys_count_| and the last one |
| // might not have been decrypted yet. |
| int encryption_keys_count_ = 0; |
| |
| // The number of the encryption key used to encrypt incoming updates. A zero |
| // value implies no encryption. |
| int update_encryption_filter_index_ = 0; |
| |
| CancelationSignal cancelation_signal_; |
| |
| // The ModelTypeWorker being tested. |
| std::unique_ptr<ModelTypeWorker> worker_; |
| |
| // Non-owned, possibly null pointer. This object belongs to the |
| // ModelTypeWorker under test. |
| raw_ptr<MockModelTypeProcessor> mock_type_processor_ = nullptr; |
| |
| // A mock that emulates enough of the sync server that it can be used |
| // a single UpdateHandler and CommitContributor pair. In this test |
| // harness, the |worker_| is both of them. |
| std::unique_ptr<SingleTypeMockServer> mock_server_; |
| |
| // A mock to track the number of times the CommitQueue requests to |
| // sync. |
| MockNudgeHandler mock_nudge_handler_; |
| |
| bool is_processor_disconnected_ = false; |
| |
| StatusController status_controller_; |
| }; |
| |
| // Requests a commit and verifies the messages sent to the client and server as |
| // a result. |
| // |
| // This test performs sanity checks on most of the fields in these messages. |
| // For the most part this is checking that the test code behaves as expected |
| // and the |worker_| doesn't mess up its simple task of moving around these |
| // values. It makes sense to have one or two tests that are this thorough, but |
| // we shouldn't be this verbose in all tests. |
| TEST_F(ModelTypeWorkerTest, SimpleCommit) { |
| base::HistogramTester histogram_tester; |
| NormalInitialize(); |
| |
| EXPECT_EQ(0, nudge_handler()->GetNumCommitNudges()); |
| EXPECT_EQ(nullptr, worker()->GetContribution(INT_MAX)); |
| EXPECT_EQ(0U, server()->GetNumCommitMessages()); |
| EXPECT_EQ(0U, processor()->GetNumCommitResponses()); |
| histogram_tester.ExpectBucketCount( |
| GetEntityChangeHistogramNameForTest(worker()->GetModelType()), |
| ModelTypeEntityChange::kLocalCreation, 0); |
| histogram_tester.ExpectBucketCount( |
| GetEntityChangeHistogramNameForTest(worker()->GetModelType()), |
| ModelTypeEntityChange::kLocalDeletion, 0); |
| |
| worker()->NudgeForCommit(); |
| EXPECT_EQ(1, nudge_handler()->GetNumCommitNudges()); |
| |
| processor()->SetCommitRequest(GenerateCommitRequest(kTag1, kValue1)); |
| DoSuccessfulCommit(); |
| |
| const ClientTagHash client_tag_hash = GenerateTagHash(kTag1); |
| |
| // Exhaustively verify the SyncEntity sent in the commit message. |
| ASSERT_EQ(1U, server()->GetNumCommitMessages()); |
| EXPECT_EQ(1, server()->GetNthCommitMessage(0).commit().entries_size()); |
| ASSERT_TRUE(server()->HasCommitEntity(kHash1)); |
| const SyncEntity& entity = server()->GetLastCommittedEntity(kHash1); |
| EXPECT_FALSE(entity.id_string().empty()); |
| EXPECT_EQ(0, entity.version()); |
| EXPECT_NE(0, entity.mtime()); |
| EXPECT_NE(0, entity.ctime()); |
| EXPECT_FALSE(entity.name().empty()); |
| EXPECT_EQ(client_tag_hash.value(), entity.client_defined_unique_tag()); |
| EXPECT_EQ(kTag1, entity.specifics().preference().name()); |
| EXPECT_FALSE(entity.deleted()); |
| EXPECT_EQ(kValue1, entity.specifics().preference().value()); |
| |
| histogram_tester.ExpectBucketCount( |
| GetEntityChangeHistogramNameForTest(worker()->GetModelType()), |
| ModelTypeEntityChange::kLocalCreation, 1); |
| histogram_tester.ExpectBucketCount( |
| GetEntityChangeHistogramNameForTest(worker()->GetModelType()), |
| ModelTypeEntityChange::kLocalDeletion, 0); |
| |
| // Exhaustively verify the commit response returned to the model thread. |
| ASSERT_EQ(0U, processor()->GetNumCommitFailures()); |
| ASSERT_EQ(1U, processor()->GetNumCommitResponses()); |
| EXPECT_EQ(1U, processor()->GetNthCommitResponse(0).size()); |
| ASSERT_TRUE(processor()->HasCommitResponse(kHash1)); |
| const CommitResponseData& commit_response = |
| processor()->GetCommitResponse(kHash1); |
| |
| // The ID changes in a commit response to initial commit. |
| EXPECT_FALSE(commit_response.id.empty()); |
| EXPECT_NE(entity.id_string(), commit_response.id); |
| |
| EXPECT_EQ(client_tag_hash, commit_response.client_tag_hash); |
| EXPECT_LT(0, commit_response.response_version); |
| EXPECT_LT(0, commit_response.sequence_number); |
| EXPECT_FALSE(commit_response.specifics_hash.empty()); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, SimpleDelete) { |
| base::HistogramTester histogram_tester; |
| NormalInitialize(); |
| |
| // We can't delete an entity that was never committed. |
| // Step 1 is to create and commit a new entity. |
| histogram_tester.ExpectBucketCount( |
| GetEntityChangeHistogramNameForTest(worker()->GetModelType()), |
| ModelTypeEntityChange::kLocalCreation, 0); |
| histogram_tester.ExpectBucketCount( |
| GetEntityChangeHistogramNameForTest(worker()->GetModelType()), |
| ModelTypeEntityChange::kLocalDeletion, 0); |
| processor()->SetCommitRequest(GenerateCommitRequest(kTag1, kValue1)); |
| DoSuccessfulCommit(); |
| |
| histogram_tester.ExpectBucketCount( |
| GetEntityChangeHistogramNameForTest(worker()->GetModelType()), |
| ModelTypeEntityChange::kLocalCreation, 1); |
| histogram_tester.ExpectBucketCount( |
| GetEntityChangeHistogramNameForTest(worker()->GetModelType()), |
| ModelTypeEntityChange::kLocalDeletion, 0); |
| |
| ASSERT_TRUE(processor()->HasCommitResponse(kHash1)); |
| const CommitResponseData& initial_commit_response = |
| processor()->GetCommitResponse(kHash1); |
| int64_t base_version = initial_commit_response.response_version; |
| |
| // Now that we have an entity, we can delete it. |
| processor()->SetCommitRequest(GenerateDeleteRequest(kTag1)); |
| DoSuccessfulCommit(); |
| |
| histogram_tester.ExpectBucketCount( |
| GetEntityChangeHistogramNameForTest(worker()->GetModelType()), |
| ModelTypeEntityChange::kLocalCreation, 1); |
| histogram_tester.ExpectBucketCount( |
| GetEntityChangeHistogramNameForTest(worker()->GetModelType()), |
| ModelTypeEntityChange::kLocalDeletion, 1); |
| |
| // Verify the SyncEntity sent in the commit message. |
| ASSERT_EQ(2U, server()->GetNumCommitMessages()); |
| EXPECT_EQ(1, server()->GetNthCommitMessage(1).commit().entries_size()); |
| ASSERT_TRUE(server()->HasCommitEntity(kHash1)); |
| const SyncEntity& entity = server()->GetLastCommittedEntity(kHash1); |
| EXPECT_FALSE(entity.id_string().empty()); |
| EXPECT_EQ(GenerateTagHash(kTag1).value(), entity.client_defined_unique_tag()); |
| EXPECT_EQ(base_version, entity.version()); |
| EXPECT_TRUE(entity.deleted()); |
| |
| // Deletions should contain enough specifics to identify the type. |
| EXPECT_TRUE(entity.has_specifics()); |
| EXPECT_EQ(PREFERENCES, GetModelTypeFromSpecifics(entity.specifics())); |
| |
| // Verify the commit response returned to the model thread. |
| ASSERT_EQ(2U, processor()->GetNumCommitResponses()); |
| EXPECT_EQ(1U, processor()->GetNthCommitResponse(1).size()); |
| ASSERT_TRUE(processor()->HasCommitResponse(kHash1)); |
| const CommitResponseData& commit_response = |
| processor()->GetCommitResponse(kHash1); |
| |
| EXPECT_EQ(entity.id_string(), commit_response.id); |
| EXPECT_EQ(entity.client_defined_unique_tag(), |
| commit_response.client_tag_hash.value()); |
| EXPECT_EQ(entity.version(), commit_response.response_version); |
| } |
| |
| // Verifies the sending of an "initial sync done" signal. |
| TEST_F(ModelTypeWorkerTest, SendInitialSyncDone) { |
| FirstInitialize(); // Initialize with no saved sync state. |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| EXPECT_EQ(1, nudge_handler()->GetNumInitialDownloadNudges()); |
| |
| EXPECT_FALSE(worker()->IsInitialSyncEnded()); |
| |
| // Receive an update response that contains only the type root node. |
| TriggerTypeRootUpdateFromServer(); |
| |
| // One update triggered by ApplyUpdates, which the worker interprets to mean |
| // "initial sync done". This triggers a model thread update, too. |
| EXPECT_EQ(1U, processor()->GetNumUpdateResponses()); |
| |
| // The update contains one entity for the root node. |
| EXPECT_EQ(1U, processor()->GetNthUpdateResponse(0).size()); |
| |
| const ModelTypeState& state = processor()->GetNthUpdateState(0); |
| EXPECT_FALSE(state.progress_marker().token().empty()); |
| EXPECT_TRUE(state.initial_sync_done()); |
| EXPECT_TRUE(worker()->IsInitialSyncEnded()); |
| } |
| |
| // Commit two new entities in two separate commit messages. |
| TEST_F(ModelTypeWorkerTest, TwoNewItemsCommittedSeparately) { |
| NormalInitialize(); |
| |
| // Commit the first of two entities. |
| processor()->SetCommitRequest(GenerateCommitRequest(kTag1, kValue1)); |
| DoSuccessfulCommit(); |
| ASSERT_EQ(1U, server()->GetNumCommitMessages()); |
| EXPECT_EQ(1, server()->GetNthCommitMessage(0).commit().entries_size()); |
| ASSERT_TRUE(server()->HasCommitEntity(kHash1)); |
| const SyncEntity& tag1_entity = server()->GetLastCommittedEntity(kHash1); |
| |
| // Commit the second of two entities. |
| processor()->SetCommitRequest(GenerateCommitRequest(kTag2, kValue2)); |
| DoSuccessfulCommit(); |
| ASSERT_EQ(2U, server()->GetNumCommitMessages()); |
| EXPECT_EQ(1, server()->GetNthCommitMessage(1).commit().entries_size()); |
| ASSERT_TRUE(server()->HasCommitEntity(kHash2)); |
| const SyncEntity& tag2_entity = server()->GetLastCommittedEntity(kHash2); |
| |
| EXPECT_FALSE(WillCommit()); |
| |
| // The IDs assigned by the |worker_| should be unique. |
| EXPECT_NE(tag1_entity.id_string(), tag2_entity.id_string()); |
| |
| // Check that the committed specifics values are sane. |
| EXPECT_EQ(tag1_entity.specifics().preference().value(), kValue1); |
| EXPECT_EQ(tag2_entity.specifics().preference().value(), kValue2); |
| |
| // There should have been two separate commit responses sent to the model |
| // thread. They should be uninteresting, so we don't bother inspecting them. |
| EXPECT_EQ(2U, processor()->GetNumCommitResponses()); |
| } |
| |
| // Test normal update receipt code path. |
| TEST_F(ModelTypeWorkerTest, ReceiveUpdates) { |
| base::HistogramTester histogram_tester; |
| NormalInitialize(); |
| |
| histogram_tester.ExpectBucketCount( |
| GetEntityChangeHistogramNameForTest(worker()->GetModelType()), |
| ModelTypeEntityChange::kRemoteNonInitialUpdate, 0); |
| |
| const ClientTagHash tag_hash = GenerateTagHash(kTag1); |
| |
| TriggerUpdateFromServer(10, kTag1, kValue1); |
| |
| ASSERT_EQ(1U, processor()->GetNumUpdateResponses()); |
| std::vector<const UpdateResponseData*> updates_list = |
| processor()->GetNthUpdateResponse(0); |
| EXPECT_EQ(1U, updates_list.size()); |
| |
| ASSERT_TRUE(processor()->HasUpdateResponse(kHash1)); |
| const UpdateResponseData& update = processor()->GetUpdateResponse(kHash1); |
| const EntityData& entity = update.entity; |
| |
| EXPECT_FALSE(entity.id.empty()); |
| EXPECT_EQ(tag_hash, entity.client_tag_hash); |
| EXPECT_LT(0, update.response_version); |
| EXPECT_FALSE(entity.creation_time.is_null()); |
| EXPECT_FALSE(entity.modification_time.is_null()); |
| EXPECT_FALSE(entity.name.empty()); |
| EXPECT_FALSE(entity.is_deleted()); |
| EXPECT_EQ(kTag1, entity.specifics.preference().name()); |
| EXPECT_EQ(kValue1, entity.specifics.preference().value()); |
| |
| histogram_tester.ExpectBucketCount( |
| GetEntityChangeHistogramNameForTest(worker()->GetModelType()), |
| ModelTypeEntityChange::kRemoteNonInitialUpdate, 1); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, ReceiveUpdates_NoDuplicateHash) { |
| NormalInitialize(); |
| |
| TriggerPartialUpdateFromServer(10, kTag1, kValue1, kTag2, kValue2); |
| TriggerPartialUpdateFromServer(10, kTag3, kValue3); |
| |
| ApplyUpdates(); |
| |
| // Make sure all the updates arrived, in order. |
| ASSERT_EQ(1u, processor()->GetNumUpdateResponses()); |
| std::vector<const UpdateResponseData*> result = |
| processor()->GetNthUpdateResponse(0); |
| ASSERT_EQ(3u, result.size()); |
| ASSERT_TRUE(result[0]); |
| EXPECT_EQ(GenerateTagHash(kTag1), result[0]->entity.client_tag_hash); |
| ASSERT_TRUE(result[1]); |
| EXPECT_EQ(GenerateTagHash(kTag2), result[1]->entity.client_tag_hash); |
| ASSERT_TRUE(result[2]); |
| EXPECT_EQ(GenerateTagHash(kTag3), result[2]->entity.client_tag_hash); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, ReceiveUpdates_DuplicateHashWithinPartialUpdate) { |
| NormalInitialize(); |
| |
| // Note that kTag1 appears twice. |
| TriggerPartialUpdateFromServer(10, kTag1, kValue1, kTag1, kValue2); |
| |
| ApplyUpdates(); |
| |
| // Make sure the duplicate entry got de-duped, and the last one won. |
| ASSERT_EQ(1u, processor()->GetNumUpdateResponses()); |
| std::vector<const UpdateResponseData*> result = |
| processor()->GetNthUpdateResponse(0); |
| ASSERT_EQ(1u, result.size()); |
| ASSERT_TRUE(result[0]); |
| EXPECT_EQ(GenerateTagHash(kTag1), result[0]->entity.client_tag_hash); |
| EXPECT_EQ(kValue2, result[0]->entity.specifics.preference().value()); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, ReceiveUpdates_DuplicateHashAcrossPartialUpdates) { |
| NormalInitialize(); |
| |
| // Note that kTag1 appears in both partial updates. |
| TriggerPartialUpdateFromServer(10, kTag1, kValue1); |
| TriggerPartialUpdateFromServer(10, kTag1, kValue2); |
| |
| ApplyUpdates(); |
| |
| // Make sure the duplicate entry got de-duped, and the last one won. |
| ASSERT_EQ(1u, processor()->GetNumUpdateResponses()); |
| std::vector<const UpdateResponseData*> result = |
| processor()->GetNthUpdateResponse(0); |
| ASSERT_EQ(1u, result.size()); |
| ASSERT_TRUE(result[0]); |
| EXPECT_EQ(GenerateTagHash(kTag1), result[0]->entity.client_tag_hash); |
| EXPECT_EQ(kValue2, result[0]->entity.specifics.preference().value()); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, |
| ReceiveUpdates_EmptyHashNotConsideredDuplicateIfForDistinctServerIds) { |
| NormalInitialize(); |
| // First create two entities with different tags, so they get assigned |
| // different server ids. |
| SyncEntity entity1 = server()->UpdateFromServer( |
| /*version_offset=*/10, GenerateTagHash(kTag1), |
| GenerateSpecifics("key1", "value1")); |
| SyncEntity entity2 = server()->UpdateFromServer( |
| /*version_offset=*/10, GenerateTagHash(kTag2), |
| GenerateSpecifics("key2", "value2")); |
| |
| // Modify both entities to have empty tags. |
| entity1.set_client_defined_unique_tag(""); |
| entity2.set_client_defined_unique_tag(""); |
| |
| worker()->ProcessGetUpdatesResponse( |
| server()->GetProgress(), server()->GetContext(), {&entity1, &entity2}, |
| status_controller()); |
| |
| ApplyUpdates(); |
| |
| // Make sure the empty client tag hashes did *not* get de-duped. |
| ASSERT_EQ(1u, processor()->GetNumUpdateResponses()); |
| std::vector<const UpdateResponseData*> result = |
| processor()->GetNthUpdateResponse(0); |
| ASSERT_EQ(2u, result.size()); |
| ASSERT_TRUE(result[0]); |
| EXPECT_EQ(entity1.id_string(), result[0]->entity.id); |
| ASSERT_TRUE(result[1]); |
| EXPECT_EQ(entity2.id_string(), result[1]->entity.id); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, ReceiveUpdates_MultipleDuplicateHashes) { |
| NormalInitialize(); |
| |
| TriggerPartialUpdateFromServer(10, kTag1, kValue3); |
| TriggerPartialUpdateFromServer(10, kTag2, kValue3); |
| TriggerPartialUpdateFromServer(10, kTag3, kValue3); |
| |
| TriggerPartialUpdateFromServer(10, kTag1, kValue2); |
| TriggerPartialUpdateFromServer(10, kTag2, kValue2); |
| |
| TriggerPartialUpdateFromServer(10, kTag1, kValue1); |
| |
| ApplyUpdates(); |
| |
| // Make sure the duplicate entries got de-duped, and the last one won. |
| ASSERT_EQ(1u, processor()->GetNumUpdateResponses()); |
| std::vector<const UpdateResponseData*> result = |
| processor()->GetNthUpdateResponse(0); |
| ASSERT_EQ(3u, result.size()); |
| ASSERT_TRUE(result[0]); |
| ASSERT_TRUE(result[1]); |
| ASSERT_TRUE(result[2]); |
| EXPECT_EQ(GenerateTagHash(kTag1), result[0]->entity.client_tag_hash); |
| EXPECT_EQ(GenerateTagHash(kTag2), result[1]->entity.client_tag_hash); |
| EXPECT_EQ(GenerateTagHash(kTag3), result[2]->entity.client_tag_hash); |
| EXPECT_EQ(kValue1, result[0]->entity.specifics.preference().value()); |
| EXPECT_EQ(kValue2, result[1]->entity.specifics.preference().value()); |
| EXPECT_EQ(kValue3, result[2]->entity.specifics.preference().value()); |
| } |
| |
| // Covers the scenario where updates have the same client tag hash but |
| // different server IDs. This scenario is considered a bug on the server. |
| TEST_F(ModelTypeWorkerTest, |
| ReceiveUpdates_DuplicateClientTagHashesForDistinctServerIds) { |
| NormalInitialize(); |
| |
| // First create three entities with different tags, so they get assigned |
| // different server ids. |
| SyncEntity oldest_entity = server()->UpdateFromServer( |
| /*version_offset=*/10, GenerateTagHash(kTag1), |
| GenerateSpecifics("key1", "value1")); |
| SyncEntity second_newest_entity = server()->UpdateFromServer( |
| /*version_offset=*/11, GenerateTagHash(kTag2), |
| GenerateSpecifics("key2", "value2")); |
| SyncEntity newest_entity = server()->UpdateFromServer( |
| /*version_offset=*/12, GenerateTagHash(kTag3), |
| GenerateSpecifics("key3", "value3")); |
| |
| // Mimic a bug on the server by modifying all entities to have the same tag. |
| second_newest_entity.set_client_defined_unique_tag( |
| oldest_entity.client_defined_unique_tag()); |
| newest_entity.set_client_defined_unique_tag( |
| oldest_entity.client_defined_unique_tag()); |
| |
| // Send |newest_entity| in the middle position, to rule out the worker is |
| // keeping the first or last received update. |
| worker()->ProcessGetUpdatesResponse( |
| server()->GetProgress(), server()->GetContext(), |
| {&oldest_entity, &newest_entity, &second_newest_entity}, |
| status_controller()); |
| |
| ApplyUpdates(); |
| |
| // Make sure the update with latest version was kept. |
| ASSERT_EQ(1u, processor()->GetNumUpdateResponses()); |
| std::vector<const UpdateResponseData*> result = |
| processor()->GetNthUpdateResponse(0); |
| ASSERT_EQ(1u, result.size()); |
| ASSERT_TRUE(result[0]); |
| EXPECT_EQ(newest_entity.id_string(), result[0]->entity.id); |
| } |
| |
| // Covers the scenario where updates have the same GUID as originator client |
| // item ID but different server IDs. This scenario is considered a bug on the |
| // server. |
| TEST_F(ModelTypeWorkerTest, |
| ReceiveUpdates_DuplicateOriginatorClientIdForDistinctServerIds) { |
| const std::string kOriginatorClientItemId = base::GenerateGUID(); |
| const std::string kURL1 = "http://url1"; |
| const std::string kURL2 = "http://url2"; |
| const std::string kURL3 = "http://url3"; |
| const std::string kServerId1 = "serverid1"; |
| const std::string kServerId2 = "serverid2"; |
| const std::string kServerId3 = "serverid3"; |
| |
| NormalInitialize(); |
| |
| sync_pb::SyncEntity oldest_entity; |
| sync_pb::SyncEntity second_newest_entity; |
| sync_pb::SyncEntity newest_entity; |
| |
| oldest_entity.set_version(1000); |
| second_newest_entity.set_version(1001); |
| newest_entity.set_version(1002); |
| |
| // Generate entities with the same originator client item ID. |
| oldest_entity.set_id_string(kServerId1); |
| second_newest_entity.set_id_string(kServerId2); |
| newest_entity.set_id_string(kServerId3); |
| oldest_entity.mutable_specifics()->mutable_bookmark()->set_url(kURL1); |
| second_newest_entity.mutable_specifics()->mutable_bookmark()->set_url(kURL2); |
| newest_entity.mutable_specifics()->mutable_bookmark()->set_url(kURL3); |
| oldest_entity.set_originator_client_item_id(kOriginatorClientItemId); |
| second_newest_entity.set_originator_client_item_id(kOriginatorClientItemId); |
| newest_entity.set_originator_client_item_id(kOriginatorClientItemId); |
| |
| // Send |newest_entity| in the middle position, to rule out the worker is |
| // keeping the first or last received update. |
| worker()->ProcessGetUpdatesResponse( |
| server()->GetProgress(), server()->GetContext(), |
| {&oldest_entity, &newest_entity, &second_newest_entity}, |
| status_controller()); |
| |
| ApplyUpdates(); |
| |
| // Make sure the update with latest version was kept. |
| ASSERT_EQ(1u, processor()->GetNumUpdateResponses()); |
| std::vector<const UpdateResponseData*> result = |
| processor()->GetNthUpdateResponse(0); |
| ASSERT_EQ(1u, result.size()); |
| ASSERT_TRUE(result[0]); |
| EXPECT_EQ(newest_entity.id_string(), result[0]->entity.id); |
| } |
| |
| // Covers the scenario where two updates have the same originator client item ID |
| // but different originator cache GUIDs. This is only possible for legacy |
| // bookmarks created before 2015. |
| TEST_F( |
| ModelTypeWorkerTest, |
| ReceiveUpdates_DuplicateOriginatorClientIdForDistinctOriginatorCacheGuids) { |
| const std::string kOriginatorClientItemId = "1"; |
| const std::string kURL1 = "http://url1"; |
| const std::string kURL2 = "http://url2"; |
| const std::string kServerId1 = "serverid1"; |
| const std::string kServerId2 = "serverid2"; |
| |
| NormalInitialize(); |
| |
| sync_pb::SyncEntity entity1; |
| sync_pb::SyncEntity entity2; |
| |
| // Generate two entities with the same originator client item ID. |
| entity1.set_id_string(kServerId1); |
| entity2.set_id_string(kServerId2); |
| entity1.mutable_specifics()->mutable_bookmark()->set_url(kURL1); |
| entity2.mutable_specifics()->mutable_bookmark()->set_url(kURL2); |
| entity1.set_originator_cache_guid(base::GenerateGUID()); |
| entity2.set_originator_cache_guid(base::GenerateGUID()); |
| entity1.set_originator_client_item_id(kOriginatorClientItemId); |
| entity2.set_originator_client_item_id(kOriginatorClientItemId); |
| |
| worker()->ProcessGetUpdatesResponse( |
| server()->GetProgress(), server()->GetContext(), {&entity1, &entity2}, |
| status_controller()); |
| |
| ApplyUpdates(); |
| |
| // Both updates should have made through. |
| ASSERT_EQ(1u, processor()->GetNumUpdateResponses()); |
| EXPECT_EQ(2u, processor()->GetNthUpdateResponse(0).size()); |
| } |
| |
| // Test that an update download coming in multiple parts gets accumulated into |
| // one call to the processor. |
| TEST_F(ModelTypeWorkerTest, ReceiveMultiPartUpdates) { |
| NormalInitialize(); |
| |
| // A partial update response doesn't pass anything to the processor. |
| TriggerPartialUpdateFromServer(10, kTag1, kValue1); |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // Trigger the completion of the update. |
| TriggerUpdateFromServer(10, kTag2, kValue2); |
| |
| // Processor received exactly one update with entities in the right order. |
| ASSERT_EQ(1U, processor()->GetNumUpdateResponses()); |
| std::vector<const UpdateResponseData*> updates = |
| processor()->GetNthUpdateResponse(0); |
| ASSERT_EQ(2U, updates.size()); |
| ASSERT_TRUE(updates[0]); |
| EXPECT_EQ(GenerateTagHash(kTag1), updates[0]->entity.client_tag_hash); |
| ASSERT_TRUE(updates[1]); |
| EXPECT_EQ(GenerateTagHash(kTag2), updates[1]->entity.client_tag_hash); |
| |
| // A subsequent update doesn't pass the same entities again. |
| TriggerUpdateFromServer(10, kTag3, kValue3); |
| ASSERT_EQ(2U, processor()->GetNumUpdateResponses()); |
| updates = processor()->GetNthUpdateResponse(1); |
| ASSERT_EQ(1U, updates.size()); |
| ASSERT_TRUE(updates[0]); |
| EXPECT_EQ(GenerateTagHash(kTag3), updates[0]->entity.client_tag_hash); |
| } |
| |
| // Test that updates with no entities behave correctly. |
| TEST_F(ModelTypeWorkerTest, EmptyUpdates) { |
| NormalInitialize(); |
| |
| server()->SetProgressMarkerToken("token2"); |
| DeliverRawUpdates(SyncEntityList()); |
| ASSERT_EQ(1U, processor()->GetNumUpdateResponses()); |
| EXPECT_EQ( |
| server()->GetProgress().SerializeAsString(), |
| processor()->GetNthUpdateState(0).progress_marker().SerializeAsString()); |
| } |
| |
| // Test commit of encrypted updates. |
| TEST_F(ModelTypeWorkerTest, EncryptedCommit) { |
| NormalInitialize(); |
| |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // Init the Cryptographer, it'll cause the EKN to be pushed. |
| AddPendingKey(); |
| DecryptPendingKey(); |
| ASSERT_EQ(1U, processor()->GetNumUpdateResponses()); |
| EXPECT_EQ(default_encryption_key_name(), |
| processor()->GetNthUpdateState(0).encryption_key_name()); |
| |
| // Normal commit request stuff. |
| processor()->SetCommitRequest(GenerateCommitRequest(kTag1, kValue1)); |
| DoSuccessfulCommit(); |
| ASSERT_EQ(1U, server()->GetNumCommitMessages()); |
| EXPECT_EQ(1, server()->GetNthCommitMessage(0).commit().entries_size()); |
| ASSERT_TRUE(server()->HasCommitEntity(kHash1)); |
| const SyncEntity& tag1_entity = server()->GetLastCommittedEntity(kHash1); |
| |
| EXPECT_TRUE(tag1_entity.specifics().has_encrypted()); |
| |
| // The title should be overwritten. |
| EXPECT_EQ(tag1_entity.name(), "encrypted"); |
| |
| // The type should be set, but there should be no non-encrypted contents. |
| EXPECT_TRUE(tag1_entity.specifics().has_preference()); |
| EXPECT_FALSE(tag1_entity.specifics().preference().has_name()); |
| EXPECT_FALSE(tag1_entity.specifics().preference().has_value()); |
| } |
| |
| // Test commit of encrypted tombstone. |
| TEST_F(ModelTypeWorkerTest, EncryptedDelete) { |
| NormalInitialize(); |
| |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // Init the Cryptographer, it'll cause the EKN to be pushed. |
| AddPendingKey(); |
| DecryptPendingKey(); |
| ASSERT_EQ(1U, processor()->GetNumUpdateResponses()); |
| EXPECT_EQ(default_encryption_key_name(), |
| processor()->GetNthUpdateState(0).encryption_key_name()); |
| |
| // Normal commit request stuff. |
| processor()->SetCommitRequest(GenerateDeleteRequest(kTag1)); |
| DoSuccessfulCommit(); |
| ASSERT_EQ(1U, server()->GetNumCommitMessages()); |
| EXPECT_EQ(1, server()->GetNthCommitMessage(0).commit().entries_size()); |
| ASSERT_TRUE(server()->HasCommitEntity(kHash1)); |
| const SyncEntity& tag1_entity = server()->GetLastCommittedEntity(kHash1); |
| |
| EXPECT_FALSE(tag1_entity.specifics().has_encrypted()); |
| |
| // The title should be overwritten. |
| EXPECT_EQ(tag1_entity.name(), "encrypted"); |
| } |
| |
| // Test that updates are not delivered to the processor when encryption is |
| // required but unavailable. |
| TEST_F(ModelTypeWorkerTest, EncryptionBlocksUpdates) { |
| NormalInitialize(); |
| |
| // Update encryption key name, should be blocked. |
| AddPendingKey(); |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // Receive an encrypted update with that new key, which we can't access. |
| SetUpdateEncryptionFilter(1); |
| TriggerUpdateFromServer(10, kTag1, kValue1); |
| |
| // At this point, the cryptographer does not have access to the key, so the |
| // updates will be undecryptable. This should block all updates. |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // Update local cryptographer, verify everything is pushed to processor. |
| DecryptPendingKey(); |
| ASSERT_EQ(1U, processor()->GetNumUpdateResponses()); |
| std::vector<const UpdateResponseData*> updates_list = |
| processor()->GetNthUpdateResponse(0); |
| EXPECT_EQ( |
| server()->GetProgress().SerializeAsString(), |
| processor()->GetNthUpdateState(0).progress_marker().SerializeAsString()); |
| } |
| |
| // Test that local changes are not committed when encryption is required but |
| // unavailable. |
| TEST_F(ModelTypeWorkerTest, EncryptionBlocksCommits) { |
| NormalInitialize(); |
| |
| AddPendingKey(); |
| |
| // We know encryption is in use on this account, but don't have the necessary |
| // encryption keys. The worker should refuse to commit. |
| worker()->NudgeForCommit(); |
| EXPECT_EQ(0, nudge_handler()->GetNumCommitNudges()); |
| EXPECT_FALSE(WillCommit()); |
| |
| // Once the cryptographer is returned to a normal state, we should be able to |
| // commit again. |
| DecryptPendingKey(); |
| EXPECT_EQ(1, nudge_handler()->GetNumCommitNudges()); |
| |
| // Verify the committed entity was properly encrypted. |
| processor()->SetCommitRequest(GenerateCommitRequest(kTag1, kValue1)); |
| DoSuccessfulCommit(); |
| ASSERT_EQ(1U, server()->GetNumCommitMessages()); |
| EXPECT_EQ(1, server()->GetNthCommitMessage(0).commit().entries_size()); |
| ASSERT_TRUE(server()->HasCommitEntity(kHash1)); |
| const SyncEntity& tag1_entity = server()->GetLastCommittedEntity(kHash1); |
| EXPECT_TRUE(tag1_entity.specifics().has_encrypted()); |
| EXPECT_EQ(tag1_entity.name(), "encrypted"); |
| EXPECT_TRUE(tag1_entity.specifics().has_preference()); |
| EXPECT_FALSE(tag1_entity.specifics().preference().has_name()); |
| EXPECT_FALSE(tag1_entity.specifics().preference().has_value()); |
| } |
| |
| // Test the receipt of decryptable entities. |
| TEST_F(ModelTypeWorkerTest, ReceiveDecryptableEntities) { |
| NormalInitialize(); |
| |
| // Create a new Nigori and allow the cryptographer to decrypt it. |
| AddPendingKey(); |
| DecryptPendingKey(); |
| |
| // First, receive an unencrypted entry. |
| TriggerUpdateFromServer(10, kTag1, kValue1); |
| |
| // Test some basic properties regarding the update. |
| ASSERT_TRUE(processor()->HasUpdateResponse(kHash1)); |
| const UpdateResponseData& update1 = processor()->GetUpdateResponse(kHash1); |
| EXPECT_EQ(kTag1, update1.entity.specifics.preference().name()); |
| EXPECT_EQ(kValue1, update1.entity.specifics.preference().value()); |
| EXPECT_TRUE(update1.encryption_key_name.empty()); |
| |
| // Set received updates to be encrypted using the new nigori. |
| SetUpdateEncryptionFilter(1); |
| |
| // This next update will be encrypted. |
| TriggerUpdateFromServer(10, kTag2, kValue2); |
| |
| // Test its basic features and the value of encryption_key_name. |
| ASSERT_TRUE(processor()->HasUpdateResponse(kHash2)); |
| const UpdateResponseData& update2 = processor()->GetUpdateResponse(kHash2); |
| EXPECT_EQ(kTag2, update2.entity.specifics.preference().name()); |
| EXPECT_EQ(kValue2, update2.entity.specifics.preference().value()); |
| EXPECT_FALSE(update2.encryption_key_name.empty()); |
| } |
| |
| // Test the receipt of decryptable entities, and that the worker will keep the |
| // entities until the decryption key arrives. |
| TEST_F(ModelTypeWorkerTest, |
| ReceiveDecryptableEntitiesShouldWaitTillKeyArrives) { |
| NormalInitialize(); |
| |
| // This next update will be encrypted using the second key. |
| SetUpdateEncryptionFilter(2); |
| TriggerUpdateFromServer(10, kTag1, kValue1); |
| |
| // Worker cannot decrypt it. |
| EXPECT_FALSE(processor()->HasUpdateResponse(kHash1)); |
| |
| // Allow the cryptographer to decrypt using the first key. |
| AddPendingKey(); |
| DecryptPendingKey(); |
| |
| // Worker still cannot decrypt it. |
| EXPECT_FALSE(processor()->HasUpdateResponse(kHash1)); |
| |
| // Allow the cryptographer to decrypt using the second key. |
| AddPendingKey(); |
| DecryptPendingKey(); |
| |
| // The worker can now decrypt the update and forward it to the processor. |
| EXPECT_TRUE(processor()->HasUpdateResponse(kHash1)); |
| } |
| |
| // Test initializing a CommitQueue with a cryptographer at startup. |
| TEST_F(ModelTypeWorkerTest, InitializeWithCryptographer) { |
| // Set up some encryption state. |
| AddPendingKey(); |
| DecryptPendingKey(); |
| |
| // Then initialize. |
| NormalInitialize(); |
| |
| // The worker should tell the model thread about encryption as soon as |
| // possible, so that it will have the chance to re-encrypt local data if |
| // necessary. |
| ASSERT_EQ(1U, processor()->GetNumUpdateResponses()); |
| EXPECT_EQ(default_encryption_key_name(), |
| processor()->GetNthUpdateState(0).encryption_key_name()); |
| } |
| |
| // Test initialzing with a cryptographer that is not ready. |
| TEST_F(ModelTypeWorkerTest, InitializeWithPendingCryptographer) { |
| // Only add a pending key, cryptographer will not be ready. |
| AddPendingKey(); |
| |
| // Then initialize. |
| NormalInitialize(); |
| |
| // Shouldn't be informed of the EKN, since there's a pending key. |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // Init the cryptographer, it'll push the EKN. |
| DecryptPendingKey(); |
| ASSERT_EQ(1U, processor()->GetNumUpdateResponses()); |
| EXPECT_EQ(default_encryption_key_name(), |
| processor()->GetNthUpdateState(0).encryption_key_name()); |
| } |
| |
| // Test initializing with a cryptographer on first startup. |
| TEST_F(ModelTypeWorkerTest, FirstInitializeWithCryptographer) { |
| // Set up a Cryptographer that's good to go. |
| AddPendingKey(); |
| DecryptPendingKey(); |
| |
| // Initialize with initial sync done to false. |
| FirstInitialize(); |
| |
| // Shouldn't be informed of the EKN, since normal activation will drive this. |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // Now perform first sync and make sure the EKN makes it. |
| TriggerTypeRootUpdateFromServer(); |
| ASSERT_EQ(1U, processor()->GetNumUpdateResponses()); |
| EXPECT_EQ(default_encryption_key_name(), |
| processor()->GetNthUpdateState(0).encryption_key_name()); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, CryptographerDuringInitialization) { |
| // Initialize with initial sync done to false. |
| FirstInitialize(); |
| |
| // Set up the Cryptographer logic after initialization but before first sync. |
| AddPendingKey(); |
| DecryptPendingKey(); |
| |
| // Shouldn't be informed of the EKN, since normal activation will drive this. |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // Now perform first sync and make sure the EKN makes it. |
| TriggerTypeRootUpdateFromServer(); |
| ASSERT_EQ(1U, processor()->GetNumUpdateResponses()); |
| EXPECT_EQ(default_encryption_key_name(), |
| processor()->GetNthUpdateState(0).encryption_key_name()); |
| } |
| |
| // Receive updates that are initially undecryptable, then ensure they get |
| // delivered to the model thread upon ApplyUpdates() after decryption becomes |
| // possible. |
| TEST_F(ModelTypeWorkerTest, ReceiveUndecryptableEntries) { |
| NormalInitialize(); |
| |
| // Receive a new foreign encryption key that we can't decrypt. |
| AddPendingKey(); |
| |
| // Receive an encrypted update with that new key, which we can't access. |
| SetUpdateEncryptionFilter(1); |
| TriggerUpdateFromServer(10, kTag1, kValue1); |
| |
| // At this point, the cryptographer does not have access to the key, so the |
| // updates will be undecryptable. This will block all updates. |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // The update should indicate that the cryptographer is ready. |
| DecryptPendingKey(); |
| EXPECT_EQ(1U, processor()->GetNumUpdateResponses()); |
| ASSERT_TRUE(processor()->HasUpdateResponse(kHash1)); |
| const UpdateResponseData& update = processor()->GetUpdateResponse(kHash1); |
| EXPECT_EQ(kTag1, update.entity.specifics.preference().name()); |
| EXPECT_EQ(kValue1, update.entity.specifics.preference().value()); |
| EXPECT_EQ(default_encryption_key_name(), update.encryption_key_name); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, OverwriteUndecryptableUpdateWithDecryptableOne) { |
| NormalInitialize(); |
| |
| // The cryptographer can decrypt data encrypted with key 1. |
| AddPendingKey(); |
| DecryptPendingKey(); |
| // The worker receives an update encrypted with an unknown key 2. |
| SetUpdateEncryptionFilter(2); |
| TriggerUpdateFromServer(10, kTag1, kValue1); |
| // The data can't be decrypted yet. |
| ASSERT_FALSE(processor()->HasUpdateResponse(kHash1)); |
| |
| // The server sends an update for the same server id now encrypted with key 1. |
| SetUpdateEncryptionFilter(1); |
| TriggerUpdateFromServer(10, kTag1, kValue1); |
| // The previous undecryptable update should be overwritten, unblocking the |
| // worker. |
| EXPECT_TRUE(processor()->HasUpdateResponse(kHash1)); |
| } |
| |
| // Verify that corrupted encrypted updates don't cause crashes. |
| TEST_F(ModelTypeWorkerTest, ReceiveCorruptEncryption) { |
| // Initialize the worker with basic encryption state. |
| NormalInitialize(); |
| AddPendingKey(); |
| DecryptPendingKey(); |
| |
| // Manually create an update. |
| SyncEntity entity; |
| entity.set_client_defined_unique_tag(GenerateTagHash(kTag1).value()); |
| entity.set_id_string("SomeID"); |
| entity.set_version(1); |
| entity.set_ctime(1000); |
| entity.set_mtime(1001); |
| entity.set_name("encrypted"); |
| entity.set_deleted(false); |
| |
| // Encrypt it. |
| *entity.mutable_specifics() = GenerateSpecifics(kTag1, kValue1); |
| EncryptUpdateWithNthKey(1, entity.mutable_specifics()); |
| |
| // Replace a few bytes to corrupt it. |
| entity.mutable_specifics()->mutable_encrypted()->mutable_blob()->replace( |
| 0, 4, "xyz!"); |
| |
| // If a corrupt update could trigger a crash, this is where it would happen. |
| DeliverRawUpdates({&entity}); |
| |
| EXPECT_FALSE(processor()->HasUpdateResponse(kHash1)); |
| |
| // Deliver a non-corrupt update to see if everything still works. |
| SetUpdateEncryptionFilter(1); |
| TriggerUpdateFromServer(10, kTag1, kValue1); |
| EXPECT_TRUE(processor()->HasUpdateResponse(kHash1)); |
| } |
| |
| // See crbug.com/1178418 for more context. |
| TEST_F(ModelTypeWorkerTest, DecryptUpdateIfPossibleDespiteEncryptionDisabled) { |
| // Make key 1 available to the underlying cryptographer without actually |
| // enabling encryption for the worker. |
| AddPendingKeyWithoutEnablingEncryption(); |
| DecryptPendingKey(); |
| NormalInitialize(); |
| ASSERT_FALSE(worker()->IsEncryptionEnabledForTest()); |
| |
| // Send an update encrypted with the known key. |
| SyncEntity update; |
| update.set_id_string("1"); |
| EncryptUpdateWithNthKey(1, update.mutable_specifics()); |
| worker()->ProcessGetUpdatesResponse(server()->GetProgress(), |
| server()->GetContext(), {&update}, |
| status_controller()); |
| worker()->ApplyUpdates(status_controller()); |
| |
| // Even though encryption is disabled for this worker, it should decrypt the |
| // update and pass it on to the processor. |
| EXPECT_FALSE(worker()->BlockForEncryption()); |
| EXPECT_EQ(1u, processor()->GetNumUpdateResponses()); |
| EXPECT_EQ(1u, processor()->GetNthUpdateResponse(0).size()); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, TimeUntilEncryptionKeyFoundMetric) { |
| base::HistogramTester histogram_tester; |
| NormalInitialize(); |
| int get_updates_while_should_have_been_known = 0; |
| |
| // Send a GetUpdatesResponse containing data encrypted with an unknown key. |
| // The cryptographer doesn't have pending keys, so in theory this key should |
| // have been known by now. This will cause |
| // |get_updates_while_should_have_been_known| to be incremented by the end |
| // of this GetUpdates cycle. |
| SetUpdateEncryptionFilter(1); |
| TriggerPartialUpdateFromServer(10, kTag1, kValue1); |
| |
| // The fact that the data type is now blocked should have been recorded. |
| histogram_tester.ExpectUniqueSample( |
| "Sync.ModelTypeBlockedDueToUndecryptableUpdate", |
| ModelTypeForHistograms::kPreferences, 1); |
| |
| // Send empty GetUpdatesResponse. The counter shouldn't change. |
| worker()->ProcessGetUpdatesResponse( |
| server()->GetProgress(), server()->GetContext(), {}, status_controller()); |
| |
| // Finish the GetUpdates cycle. The counter should be set to 1. |
| ApplyUpdates(); |
| get_updates_while_should_have_been_known++; |
| |
| // An empty GetUpdates cycle. The counter should be set to 2. |
| worker()->ProcessGetUpdatesResponse( |
| server()->GetProgress(), server()->GetContext(), {}, status_controller()); |
| ApplyUpdates(); |
| get_updates_while_should_have_been_known++; |
| |
| // Send the Nigori containing the missing key. The key isn't available yet |
| // though. |
| AddPendingKey(); |
| |
| // Another empty GetUpdates cycle. This one shouldn't be counted, since the |
| // cryptographer now knows it's lacking some keys. |
| worker()->ProcessGetUpdatesResponse( |
| server()->GetProgress(), server()->GetContext(), {}, status_controller()); |
| ApplyUpdates(); |
| |
| // Double check the histogram hasn't been recorded so far. |
| EXPECT_TRUE(histogram_tester |
| .GetAllSamples("Sync.ModelTypeTimeUntilEncryptionKeyFound2") |
| .empty()); |
| EXPECT_TRUE(histogram_tester |
| .GetAllSamples( |
| "Sync.ModelTypeTimeUntilEncryptionKeyFound2.PREFERENCE") |
| .empty()); |
| |
| // Make the key available. The correct number of GetUpdates cycles should |
| // have been recorded. |
| DecryptPendingKey(); |
| ASSERT_EQ(2, get_updates_while_should_have_been_known); |
| histogram_tester.ExpectUniqueSample( |
| "Sync.ModelTypeTimeUntilEncryptionKeyFound2", |
| get_updates_while_should_have_been_known, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Sync.ModelTypeTimeUntilEncryptionKeyFound2.PREFERENCE", |
| get_updates_while_should_have_been_known, 1); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, IgnoreUpdatesEncryptedWithKeysMissingForTooLong) { |
| base::HistogramTester histogram_tester; |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature(kIgnoreSyncEncryptionKeysLongMissing); |
| |
| NormalInitialize(); |
| worker()->SetMinGetUpdatesToIgnoreKeyForTest(2); |
| |
| // Send an update encrypted with a key that shall remain unknown. |
| SetUpdateEncryptionFilter(1); |
| TriggerUpdateFromServer(10, kTag1, kValue1); |
| |
| // The undecryptable update has been around for only 1 GetUpdatesResponse, so |
| // the worker is still blocked. |
| EXPECT_TRUE(worker()->BlockForEncryption()); |
| |
| // Send empty GetUpdates, reaching the threshold of 2. |
| worker()->ProcessGetUpdatesResponse( |
| server()->GetProgress(), server()->GetContext(), {}, status_controller()); |
| ApplyUpdates(); |
| |
| // The undecryptable update should have been dropped and the worker is no |
| // longer blocked. |
| EXPECT_FALSE(worker()->BlockForEncryption()); |
| |
| // Should have recorded that 1 entity was dropped. |
| histogram_tester.ExpectUniqueSample( |
| "Sync.ModelTypeUndecryptablePendingUpdatesDropped", 1, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Sync.ModelTypeUndecryptablePendingUpdatesDropped.PREFERENCE", 1, 1); |
| |
| // From now on, incoming updates encrypted with the missing key don't block |
| // the worker. |
| TriggerUpdateFromServer(10, kTag2, kValue2); |
| EXPECT_FALSE(worker()->BlockForEncryption()); |
| |
| // Should have recorded that 1 incoming update was ignored. |
| histogram_tester.ExpectUniqueSample( |
| "Sync.ModelTypeUpdateDrop.DecryptionPendingForTooLong", |
| ModelTypeForHistograms::kPreferences, 1); |
| } |
| |
| // Test that processor has been disconnected from Sync when worker got |
| // disconnected. |
| TEST_F(ModelTypeWorkerTest, DisconnectProcessorFromSyncTest) { |
| // Initialize the worker with basic state. |
| NormalInitialize(); |
| EXPECT_FALSE(IsProcessorDisconnected()); |
| ResetWorker(); |
| EXPECT_TRUE(IsProcessorDisconnected()); |
| } |
| |
| // Test that deleted entity can be recreated again. |
| TEST_F(ModelTypeWorkerTest, RecreateDeletedEntity) { |
| NormalInitialize(); |
| |
| // Create, then delete entity. |
| processor()->SetCommitRequest(GenerateCommitRequest(kTag1, kValue1)); |
| DoSuccessfulCommit(); |
| |
| processor()->SetCommitRequest(GenerateDeleteRequest(kTag1)); |
| DoSuccessfulCommit(); |
| |
| // Verify that entity got deleted from the server. |
| { |
| const SyncEntity& entity = server()->GetLastCommittedEntity(kHash1); |
| EXPECT_TRUE(entity.deleted()); |
| } |
| |
| // Create the same entity again. |
| processor()->SetCommitRequest(GenerateCommitRequest(kTag1, kValue1)); |
| DoSuccessfulCommit(); |
| // Verify that there is a valid entity on the server. |
| { |
| const SyncEntity& entity = server()->GetLastCommittedEntity(kHash1); |
| EXPECT_FALSE(entity.deleted()); |
| } |
| } |
| |
| TEST_F(ModelTypeWorkerTest, CommitOnly) { |
| base::HistogramTester histogram_tester; |
| ModelType model_type = USER_EVENTS; |
| InitializeCommitOnly(model_type); |
| |
| int id = 123456789; |
| EntitySpecifics specifics; |
| specifics.mutable_user_event()->set_event_time_usec(id); |
| processor()->SetCommitRequest(GenerateCommitRequest(kHash1, specifics)); |
| DoSuccessfulCommit(); |
| |
| ASSERT_EQ(1U, server()->GetNumCommitMessages()); |
| EXPECT_EQ(1, server()->GetNthCommitMessage(0).commit().entries_size()); |
| const SyncEntity entity = |
| server()->GetNthCommitMessage(0).commit().entries(0); |
| |
| EXPECT_FALSE(entity.has_ctime()); |
| EXPECT_FALSE(entity.has_deleted()); |
| EXPECT_FALSE(entity.has_folder()); |
| EXPECT_FALSE(entity.has_id_string()); |
| EXPECT_FALSE(entity.has_mtime()); |
| EXPECT_FALSE(entity.has_version()); |
| EXPECT_FALSE(entity.has_name()); |
| EXPECT_TRUE(entity.specifics().has_user_event()); |
| EXPECT_EQ(id, entity.specifics().user_event().event_time_usec()); |
| |
| histogram_tester.ExpectBucketCount( |
| GetEntityChangeHistogramNameForTest(model_type), |
| ModelTypeEntityChange::kLocalCreation, 1); |
| histogram_tester.ExpectBucketCount( |
| GetEntityChangeHistogramNameForTest(model_type), |
| ModelTypeEntityChange::kLocalDeletion, 0); |
| |
| ASSERT_EQ(1U, processor()->GetNumCommitResponses()); |
| EXPECT_EQ(1U, processor()->GetNthCommitResponse(0).size()); |
| ASSERT_TRUE(processor()->HasCommitResponse(kHash1)); |
| const CommitResponseData& commit_response = |
| processor()->GetCommitResponse(kHash1); |
| EXPECT_EQ(kHash1, commit_response.client_tag_hash); |
| EXPECT_FALSE(commit_response.specifics_hash.empty()); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, ShouldPropagateCommitFailure) { |
| NormalInitialize(); |
| processor()->SetCommitRequest(GenerateCommitRequest(kTag1, kValue1)); |
| |
| DoCommitFailure(); |
| |
| EXPECT_EQ(1U, processor()->GetNumCommitFailures()); |
| EXPECT_EQ(0U, processor()->GetNumCommitResponses()); |
| } |
| |
| TEST(ModelTypeWorkerPopulateUpdateResponseDataTest, |
| NonBookmarkNorWalletSucceeds) { |
| sync_pb::SyncEntity entity; |
| |
| entity.set_id_string("SomeID"); |
| entity.set_parent_id_string("ParentID"); |
| entity.set_folder(false); |
| entity.set_version(1); |
| entity.set_client_defined_unique_tag("CLIENT_TAG"); |
| entity.set_server_defined_unique_tag("SERVER_TAG"); |
| entity.set_deleted(false); |
| *entity.mutable_specifics() = GenerateSpecifics(kTag1, kValue1); |
| UpdateResponseData response_data; |
| |
| base::HistogramTester histogram_tester; |
| |
| EXPECT_EQ(ModelTypeWorker::SUCCESS, |
| ModelTypeWorker::PopulateUpdateResponseData( |
| FakeCryptographer(), PREFERENCES, entity, &response_data)); |
| const EntityData& data = response_data.entity; |
| EXPECT_FALSE(data.id.empty()); |
| EXPECT_FALSE(data.legacy_parent_id.empty()); |
| EXPECT_EQ("CLIENT_TAG", data.client_tag_hash.value()); |
| EXPECT_EQ("SERVER_TAG", data.server_defined_unique_tag); |
| EXPECT_FALSE(data.is_deleted()); |
| EXPECT_EQ(kTag1, data.specifics.preference().name()); |
| EXPECT_EQ(kValue1, data.specifics.preference().value()); |
| } |
| |
| TEST(ModelTypeWorkerPopulateUpdateResponseDataTest, BookmarkTombstone) { |
| sync_pb::SyncEntity entity; |
| // Production server sets the name to be "tombstone" for all tombstones. |
| entity.set_name("tombstone"); |
| entity.set_id_string("SomeID"); |
| entity.set_parent_id_string("ParentID"); |
| entity.set_folder(false); |
| *entity.mutable_unique_position() = |
| UniquePosition::InitialPosition(UniquePosition::RandomSuffix()).ToProto(); |
| entity.set_version(1); |
| entity.set_server_defined_unique_tag("SERVER_TAG"); |
| // Mark this as a tombstone. |
| entity.set_deleted(true); |
| // Add default value field for a Bookmark. |
| entity.mutable_specifics()->mutable_bookmark(); |
| |
| UpdateResponseData response_data; |
| EXPECT_EQ(ModelTypeWorker::SUCCESS, |
| ModelTypeWorker::PopulateUpdateResponseData( |
| FakeCryptographer(), BOOKMARKS, entity, &response_data)); |
| |
| const EntityData& data = response_data.entity; |
| // A tombstone should remain a tombstone after populating the response data. |
| EXPECT_TRUE(data.is_deleted()); |
| } |
| |
| TEST(ModelTypeWorkerPopulateUpdateResponseDataTest, |
| BookmarkWithUniquePositionInSyncEntity) { |
| const UniquePosition kUniquePosition = |
| UniquePosition::InitialPosition(UniquePosition::RandomSuffix()); |
| sync_pb::SyncEntity entity; |
| *entity.mutable_unique_position() = kUniquePosition.ToProto(); |
| entity.set_client_defined_unique_tag("CLIENT_TAG"); |
| entity.set_server_defined_unique_tag("SERVER_TAG"); |
| entity.mutable_specifics()->mutable_bookmark(); |
| |
| UpdateResponseData response_data; |
| |
| EXPECT_EQ(ModelTypeWorker::SUCCESS, |
| ModelTypeWorker::PopulateUpdateResponseData( |
| FakeCryptographer(), BOOKMARKS, entity, &response_data)); |
| const EntityData& data = response_data.entity; |
| EXPECT_TRUE(syncer::UniquePosition::FromProto( |
| data.specifics.bookmark().unique_position()) |
| .Equals(kUniquePosition)); |
| } |
| |
| TEST(ModelTypeWorkerPopulateUpdateResponseDataTest, |
| BookmarkWithPositionInParent) { |
| sync_pb::SyncEntity entity; |
| entity.set_position_in_parent(5); |
| entity.set_client_defined_unique_tag("CLIENT_TAG"); |
| entity.set_server_defined_unique_tag("SERVER_TAG"); |
| entity.mutable_specifics()->mutable_bookmark(); |
| |
| UpdateResponseData response_data; |
| |
| EXPECT_EQ(ModelTypeWorker::SUCCESS, |
| ModelTypeWorker::PopulateUpdateResponseData( |
| FakeCryptographer(), BOOKMARKS, entity, &response_data)); |
| const EntityData& data = response_data.entity; |
| EXPECT_TRUE(syncer::UniquePosition::FromProto( |
| data.specifics.bookmark().unique_position()) |
| .IsValid()); |
| } |
| |
| TEST(ModelTypeWorkerPopulateUpdateResponseDataTest, |
| BookmarkWithInsertAfterItemId) { |
| sync_pb::SyncEntity entity; |
| entity.set_insert_after_item_id("ITEM_ID"); |
| entity.set_client_defined_unique_tag("CLIENT_TAG"); |
| entity.set_server_defined_unique_tag("SERVER_TAG"); |
| entity.mutable_specifics()->mutable_bookmark(); |
| |
| UpdateResponseData response_data; |
| |
| EXPECT_EQ(ModelTypeWorker::SUCCESS, |
| ModelTypeWorker::PopulateUpdateResponseData( |
| FakeCryptographer(), BOOKMARKS, entity, &response_data)); |
| const EntityData& data = response_data.entity; |
| EXPECT_TRUE(syncer::UniquePosition::FromProto( |
| data.specifics.bookmark().unique_position()) |
| .IsValid()); |
| } |
| |
| TEST(ModelTypeWorkerPopulateUpdateResponseDataTest, |
| BookmarkWithMissingPositionFallsBackToRandom) { |
| sync_pb::SyncEntity entity; |
| entity.set_client_defined_unique_tag("CLIENT_TAG"); |
| entity.set_server_defined_unique_tag("SERVER_TAG"); |
| entity.mutable_specifics()->mutable_bookmark(); |
| |
| UpdateResponseData response_data; |
| |
| EXPECT_EQ(ModelTypeWorker::SUCCESS, |
| ModelTypeWorker::PopulateUpdateResponseData( |
| FakeCryptographer(), BOOKMARKS, entity, &response_data)); |
| const EntityData& data = response_data.entity; |
| EXPECT_TRUE(syncer::UniquePosition::FromProto( |
| data.specifics.bookmark().unique_position()) |
| .IsValid()); |
| } |
| |
| TEST(ModelTypeWorkerPopulateUpdateResponseDataTest, BookmarkWithGUID) { |
| const std::string kGuid1 = base::GenerateGUID(); |
| const std::string kGuid2 = base::GenerateGUID(); |
| |
| sync_pb::SyncEntity entity; |
| |
| // Generate specifics with a GUID. |
| entity.mutable_specifics()->mutable_bookmark()->set_guid(kGuid1); |
| entity.set_originator_client_item_id(kGuid2); |
| *entity.mutable_unique_position() = |
| UniquePosition::InitialPosition(UniquePosition::RandomSuffix()).ToProto(); |
| |
| UpdateResponseData response_data; |
| |
| EXPECT_EQ(ModelTypeWorker::SUCCESS, |
| ModelTypeWorker::PopulateUpdateResponseData( |
| FakeCryptographer(), BOOKMARKS, entity, &response_data)); |
| |
| const EntityData& data = response_data.entity; |
| |
| EXPECT_EQ(kGuid1, data.specifics.bookmark().guid()); |
| EXPECT_EQ(kGuid2, data.originator_client_item_id); |
| } |
| |
| TEST(ModelTypeWorkerPopulateUpdateResponseDataTest, BookmarkWithMissingGUID) { |
| const std::string kGuid1 = base::GenerateGUID(); |
| |
| sync_pb::SyncEntity entity; |
| |
| // Generate specifics without a GUID. |
| entity.mutable_specifics()->mutable_bookmark(); |
| entity.set_originator_client_item_id(kGuid1); |
| *entity.mutable_unique_position() = |
| UniquePosition::InitialPosition(UniquePosition::RandomSuffix()).ToProto(); |
| |
| UpdateResponseData response_data; |
| |
| EXPECT_EQ(ModelTypeWorker::SUCCESS, |
| ModelTypeWorker::PopulateUpdateResponseData( |
| FakeCryptographer(), BOOKMARKS, entity, &response_data)); |
| |
| const EntityData& data = response_data.entity; |
| |
| EXPECT_EQ(kGuid1, data.originator_client_item_id); |
| EXPECT_EQ(kGuid1, data.specifics.bookmark().guid()); |
| } |
| |
| TEST(ModelTypeWorkerPopulateUpdateResponseDataTest, |
| BookmarkWithMissingGUIDAndInvalidOCII) { |
| const std::string kInvalidOCII = "INVALID OCII"; |
| |
| sync_pb::SyncEntity entity; |
| |
| // Generate specifics without a GUID and with an invalid |
| // originator_client_item_id. |
| entity.mutable_specifics()->mutable_bookmark(); |
| entity.set_originator_client_item_id(kInvalidOCII); |
| *entity.mutable_unique_position() = |
| UniquePosition::InitialPosition(UniquePosition::RandomSuffix()).ToProto(); |
| |
| UpdateResponseData response_data; |
| |
| EXPECT_EQ(ModelTypeWorker::SUCCESS, |
| ModelTypeWorker::PopulateUpdateResponseData( |
| FakeCryptographer(), BOOKMARKS, entity, &response_data)); |
| |
| const EntityData& data = response_data.entity; |
| |
| EXPECT_EQ(kInvalidOCII, data.originator_client_item_id); |
| EXPECT_TRUE(base::IsValidGUIDOutputString(data.specifics.bookmark().guid())); |
| } |
| |
| TEST(ModelTypeWorkerPopulateUpdateResponseDataTest, |
| WalletDataWithMissingClientTagHash) { |
| UpdateResponseData response_data; |
| |
| // Set up the entity with an arbitrary value for an arbitrary field in the |
| // specifics (so that it _has_ autofill wallet specifics). |
| sync_pb::SyncEntity entity; |
| entity.mutable_specifics()->mutable_autofill_wallet()->set_type( |
| sync_pb::AutofillWalletSpecifics::POSTAL_ADDRESS); |
| |
| ASSERT_EQ( |
| ModelTypeWorker::SUCCESS, |
| ModelTypeWorker::PopulateUpdateResponseData( |
| FakeCryptographer(), AUTOFILL_WALLET_DATA, entity, &response_data)); |
| |
| // The client tag hash gets filled in by the worker. |
| EXPECT_FALSE(response_data.entity.client_tag_hash.value().empty()); |
| } |
| |
| TEST(ModelTypeWorkerPopulateUpdateResponseDataTest, |
| OfferDataWithMissingClientTagHash) { |
| UpdateResponseData response_data; |
| |
| // Set up the entity with an arbitrary value for an arbitrary field in the |
| // specifics (so that it _has_ autofill offer specifics). |
| sync_pb::SyncEntity entity; |
| entity.mutable_specifics()->mutable_autofill_offer()->set_id(1234567); |
| |
| ASSERT_EQ( |
| ModelTypeWorker::SUCCESS, |
| ModelTypeWorker::PopulateUpdateResponseData( |
| FakeCryptographer(), AUTOFILL_WALLET_OFFER, entity, &response_data)); |
| |
| // The client tag hash gets filled in by the worker. |
| EXPECT_FALSE(response_data.entity.client_tag_hash.value().empty()); |
| } |
| |
| class GetLocalChangesRequestTest : public testing::Test { |
| public: |
| GetLocalChangesRequestTest(); |
| ~GetLocalChangesRequestTest() override; |
| |
| void SetUp() override; |
| void TearDown() override; |
| |
| scoped_refptr<GetLocalChangesRequest> MakeRequest(); |
| |
| void BlockingWaitForResponseOrCancelation( |
| scoped_refptr<GetLocalChangesRequest> request); |
| void ScheduleBlockingWait(scoped_refptr<GetLocalChangesRequest> request); |
| |
| protected: |
| CancelationSignal cancelation_signal_; |
| base::Thread blocking_thread_; |
| base::WaitableEvent start_event_; |
| base::WaitableEvent done_event_; |
| }; |
| |
| GetLocalChangesRequestTest::GetLocalChangesRequestTest() |
| : blocking_thread_("BlockingThread"), |
| start_event_(base::WaitableEvent::ResetPolicy::MANUAL, |
| base::WaitableEvent::InitialState::NOT_SIGNALED), |
| done_event_(base::WaitableEvent::ResetPolicy::MANUAL, |
| base::WaitableEvent::InitialState::NOT_SIGNALED) {} |
| |
| GetLocalChangesRequestTest::~GetLocalChangesRequestTest() = default; |
| |
| void GetLocalChangesRequestTest::SetUp() { |
| blocking_thread_.Start(); |
| } |
| |
| void GetLocalChangesRequestTest::TearDown() { |
| blocking_thread_.Stop(); |
| } |
| |
| scoped_refptr<GetLocalChangesRequest> |
| GetLocalChangesRequestTest::MakeRequest() { |
| return base::MakeRefCounted<GetLocalChangesRequest>(&cancelation_signal_); |
| } |
| |
| void GetLocalChangesRequestTest::BlockingWaitForResponseOrCancelation( |
| scoped_refptr<GetLocalChangesRequest> request) { |
| start_event_.Signal(); |
| request->WaitForResponseOrCancelation(); |
| done_event_.Signal(); |
| } |
| |
| void GetLocalChangesRequestTest::ScheduleBlockingWait( |
| scoped_refptr<GetLocalChangesRequest> request) { |
| blocking_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &GetLocalChangesRequestTest::BlockingWaitForResponseOrCancelation, |
| base::Unretained(this), request)); |
| } |
| |
| // Tests that request doesn't block when cancelation signal is already signaled. |
| TEST_F(GetLocalChangesRequestTest, CancelationSignaledBeforeRequest) { |
| cancelation_signal_.Signal(); |
| scoped_refptr<GetLocalChangesRequest> request = MakeRequest(); |
| request->WaitForResponseOrCancelation(); |
| EXPECT_TRUE(request->WasCancelled()); |
| } |
| |
| // Tests that signaling cancelation signal while request is blocked unblocks it. |
| TEST_F(GetLocalChangesRequestTest, CancelationSignaledAfterRequest) { |
| scoped_refptr<GetLocalChangesRequest> request = MakeRequest(); |
| ScheduleBlockingWait(request); |
| start_event_.Wait(); |
| cancelation_signal_.Signal(); |
| done_event_.Wait(); |
| EXPECT_TRUE(request->WasCancelled()); |
| } |
| |
| // Tests that setting response unblocks request. |
| TEST_F(GetLocalChangesRequestTest, SuccessfulRequest) { |
| const std::string kHash1 = "SomeHash"; |
| scoped_refptr<GetLocalChangesRequest> request = MakeRequest(); |
| ScheduleBlockingWait(request); |
| start_event_.Wait(); |
| { |
| CommitRequestDataList response; |
| response.push_back(std::make_unique<CommitRequestData>()); |
| response.back()->specifics_hash = kHash1; |
| request->SetResponse(std::move(response)); |
| } |
| done_event_.Wait(); |
| EXPECT_FALSE(request->WasCancelled()); |
| CommitRequestDataList response = request->ExtractResponse(); |
| EXPECT_EQ(1U, response.size()); |
| EXPECT_EQ(kHash1, response[0]->specifics_hash); |
| } |
| |
| // Analogous test fixture but uses PASSWORDS instead of PREFERENCES, in order |
| // to test some special encryption requirements for PASSWORDS. |
| class ModelTypeWorkerPasswordsTest : public ModelTypeWorkerTest { |
| protected: |
| const std::string kPassword = "SomePassword"; |
| |
| ModelTypeWorkerPasswordsTest() |
| : ModelTypeWorkerTest(PASSWORDS, /*is_encrypted_type=*/true) {} |
| }; |
| |
| // Similar to EncryptedCommit but tests PASSWORDS specifically, which use a |
| // different encryption mechanism. |
| TEST_F(ModelTypeWorkerPasswordsTest, PasswordCommit) { |
| NormalInitialize(); |
| |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // Init the Cryptographer, it'll cause the EKN to be pushed. |
| AddPendingKey(); |
| DecryptPendingKey(); |
| ASSERT_EQ(1U, processor()->GetNumUpdateResponses()); |
| EXPECT_EQ(default_encryption_key_name(), |
| processor()->GetNthUpdateState(0).encryption_key_name()); |
| |
| EntitySpecifics specifics; |
| sync_pb::PasswordSpecificsData* password_data = |
| specifics.mutable_password()->mutable_client_only_encrypted_data(); |
| password_data->set_signon_realm("signon_realm"); |
| |
| // Normal commit request stuff. |
| processor()->SetCommitRequest(GenerateCommitRequest(kHash1, specifics)); |
| DoSuccessfulCommit(); |
| ASSERT_EQ(1U, server()->GetNumCommitMessages()); |
| EXPECT_EQ(1, server()->GetNthCommitMessage(0).commit().entries_size()); |
| ASSERT_TRUE(server()->HasCommitEntity(kHash1)); |
| const SyncEntity& tag1_entity = server()->GetLastCommittedEntity(kHash1); |
| |
| EXPECT_FALSE(tag1_entity.specifics().has_encrypted()); |
| EXPECT_TRUE(tag1_entity.specifics().has_password()); |
| EXPECT_TRUE(tag1_entity.specifics().password().has_encrypted()); |
| |
| // The title should be overwritten. |
| EXPECT_EQ(tag1_entity.name(), "encrypted"); |
| } |
| |
| // Similar to ReceiveDecryptableEntities but for PASSWORDS, which have a custom |
| // encryption mechanism. |
| TEST_F(ModelTypeWorkerPasswordsTest, ReceiveDecryptablePasswordEntities) { |
| NormalInitialize(); |
| |
| // Create a new Nigori and allow the cryptographer to decrypt it. |
| AddPendingKey(); |
| DecryptPendingKey(); |
| |
| sync_pb::PasswordSpecificsData unencrypted_password; |
| unencrypted_password.set_password_value(kPassword); |
| sync_pb::EntitySpecifics encrypted_specifics = |
| EncryptPasswordSpecificsWithNthKey(1, unencrypted_password); |
| |
| // Receive an encrypted password, encrypted with a key that is already known. |
| SyncEntity entity = server()->UpdateFromServer( |
| /*version_offset=*/10, kHash1, encrypted_specifics); |
| worker()->ProcessGetUpdatesResponse(server()->GetProgress(), |
| server()->GetContext(), {&entity}, |
| status_controller()); |
| worker()->ApplyUpdates(status_controller()); |
| |
| // Test its basic features and the value of encryption_key_name. |
| ASSERT_TRUE(processor()->HasUpdateResponse(kHash1)); |
| const UpdateResponseData& update = processor()->GetUpdateResponse(kHash1); |
| EXPECT_FALSE(update.entity.specifics.password().has_encrypted()); |
| EXPECT_FALSE(update.entity.specifics.has_encrypted()); |
| ASSERT_TRUE( |
| update.entity.specifics.password().has_client_only_encrypted_data()); |
| EXPECT_EQ(kPassword, update.entity.specifics.password() |
| .client_only_encrypted_data() |
| .password_value()); |
| } |
| |
| // Similar to ReceiveDecryptableEntities but for PASSWORDS, which have a custom |
| // encryption mechanism. |
| TEST_F(ModelTypeWorkerPasswordsTest, |
| ReceiveDecryptablePasswordShouldWaitTillKeyArrives) { |
| NormalInitialize(); |
| |
| // Receive an encrypted password, encrypted with the second encryption key. |
| sync_pb::PasswordSpecificsData unencrypted_password; |
| unencrypted_password.set_password_value(kPassword); |
| sync_pb::EntitySpecifics encrypted_specifics = |
| EncryptPasswordSpecificsWithNthKey(2, unencrypted_password); |
| |
| SyncEntity entity = server()->UpdateFromServer( |
| /*version_offset=*/10, kHash1, encrypted_specifics); |
| worker()->ProcessGetUpdatesResponse(server()->GetProgress(), |
| server()->GetContext(), {&entity}, |
| status_controller()); |
| worker()->ApplyUpdates(status_controller()); |
| |
| // Worker cannot decrypt it. |
| EXPECT_FALSE(processor()->HasUpdateResponse(kHash1)); |
| |
| // Allow the cryptographer to decrypt using the first key. |
| AddPendingKey(); |
| DecryptPendingKey(); |
| |
| // Worker still cannot decrypt it. |
| EXPECT_FALSE(processor()->HasUpdateResponse(kHash1)); |
| |
| // Allow the cryptographer to decrypt using the second key. |
| AddPendingKey(); |
| DecryptPendingKey(); |
| |
| // The worker can now decrypt the update and forward it to the processor. |
| EXPECT_TRUE(processor()->HasUpdateResponse(kHash1)); |
| } |
| |
| // Analogous to ReceiveUndecryptableEntries but for PASSWORDS, which have a |
| // custom encryption mechanism. |
| TEST_F(ModelTypeWorkerPasswordsTest, ReceiveUndecryptablePasswordEntries) { |
| NormalInitialize(); |
| |
| // Receive a new foreign encryption key that we can't decrypt. |
| AddPendingKey(); |
| |
| sync_pb::PasswordSpecificsData unencrypted_password; |
| unencrypted_password.set_password_value(kPassword); |
| sync_pb::EntitySpecifics encrypted_specifics = |
| EncryptPasswordSpecificsWithNthKey(1, unencrypted_password); |
| |
| // Receive an encrypted update with that new key, which we can't access. |
| SyncEntity entity = server()->UpdateFromServer( |
| /*version_offset=*/10, kHash1, encrypted_specifics); |
| worker()->ProcessGetUpdatesResponse(server()->GetProgress(), |
| server()->GetContext(), {&entity}, |
| status_controller()); |
| worker()->ApplyUpdates(status_controller()); |
| |
| // At this point, the cryptographer does not have access to the key, so the |
| // updates will be undecryptable. This will block all updates. |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // The update should indicate that the cryptographer is ready. |
| DecryptPendingKey(); |
| EXPECT_EQ(1U, processor()->GetNumUpdateResponses()); |
| ASSERT_TRUE(processor()->HasUpdateResponse(kHash1)); |
| const UpdateResponseData& update = processor()->GetUpdateResponse(kHash1); |
| // Password should now be decrypted and sent to the processor. |
| EXPECT_TRUE(update.entity.specifics.has_password()); |
| EXPECT_FALSE(update.entity.specifics.password().has_encrypted()); |
| ASSERT_TRUE( |
| update.entity.specifics.password().has_client_only_encrypted_data()); |
| EXPECT_EQ(kPassword, update.entity.specifics.password() |
| .client_only_encrypted_data() |
| .password_value()); |
| } |
| |
| // Similar to ReceiveDecryptableEntities but for PASSWORDS, which have a custom |
| // encryption mechanism. |
| TEST_F(ModelTypeWorkerPasswordsTest, ReceiveCorruptedPasswordEntities) { |
| NormalInitialize(); |
| |
| sync_pb::PasswordSpecificsData unencrypted_password; |
| unencrypted_password.set_password_value(kPassword); |
| sync_pb::EntitySpecifics encrypted_specifics = |
| EncryptPasswordSpecificsWithNthKey(1, unencrypted_password); |
| // Manipulate the blob to be corrupted. |
| encrypted_specifics.mutable_password()->mutable_encrypted()->set_blob( |
| "corrupted blob"); |
| |
| // Receive an encrypted password, encrypted with a key that is already known. |
| SyncEntity entity = server()->UpdateFromServer( |
| /*version_offset=*/10, kHash1, encrypted_specifics); |
| worker()->ProcessGetUpdatesResponse(server()->GetProgress(), |
| server()->GetContext(), {&entity}, |
| status_controller()); |
| worker()->ApplyUpdates(status_controller()); |
| |
| // No updates should have reached the processor and worker is blocked for |
| // encryption because the cryptographer isn't ready yet. |
| EXPECT_FALSE(processor()->HasUpdateResponse(kHash1)); |
| EXPECT_TRUE(worker()->BlockForEncryption()); |
| |
| // Allow the cryptographer to decrypt using the first key. |
| AddPendingKey(); |
| DecryptPendingKey(); |
| |
| // Still, no updates should have reached the processor and worker is NOT |
| // blocked for encryption anymore. |
| EXPECT_FALSE(processor()->HasUpdateResponse(kHash1)); |
| EXPECT_FALSE(worker()->BlockForEncryption()); |
| } |
| |
| // Analogous test fixture but uses BOOKMARKS instead of PREFERENCES, in order |
| // to test some special encryption requirements for BOOKMARKS. |
| class ModelTypeWorkerBookmarksTest : public ModelTypeWorkerTest { |
| protected: |
| ModelTypeWorkerBookmarksTest() |
| : ModelTypeWorkerTest(BOOKMARKS, /*is_encrypted_type=*/false) {} |
| }; |
| |
| TEST_F(ModelTypeWorkerBookmarksTest, CanDecryptUpdateWithMissingBookmarkGUID) { |
| const std::string kGuid1 = base::GenerateGUID(); |
| |
| // Initialize the worker with basic encryption state. |
| NormalInitialize(); |
| AddPendingKey(); |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // Generate specifics without a GUID. |
| sync_pb::SyncEntity entity; |
| entity.mutable_specifics()->mutable_bookmark()->set_url("www.foo.com"); |
| entity.mutable_specifics() |
| ->mutable_bookmark() |
| ->set_legacy_canonicalized_title("Title"); |
| entity.set_id_string("testserverid"); |
| entity.set_originator_client_item_id(kGuid1); |
| *entity.mutable_unique_position() = |
| UniquePosition::InitialPosition(UniquePosition::RandomSuffix()).ToProto(); |
| |
| // Encrypt it. |
| EncryptUpdateWithNthKey(1, entity.mutable_specifics()); |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| DecryptPendingKey(); |
| |
| worker()->ProcessGetUpdatesResponse(server()->GetProgress(), |
| server()->GetContext(), {&entity}, |
| status_controller()); |
| worker()->ApplyUpdates(status_controller()); |
| |
| EXPECT_EQ(2U, processor()->GetNumUpdateResponses()); |
| |
| // First response should contain no updates, since ApplyUpdates() was called |
| // from within DecryptPendingKey() before any were added. |
| EXPECT_EQ(0U, processor()->GetNthUpdateResponse(0).size()); |
| EXPECT_EQ(1U, processor()->GetNthUpdateResponse(1).size()); |
| |
| EXPECT_EQ(kGuid1, processor() |
| ->GetNthUpdateResponse(1) |
| .at(0) |
| ->entity.originator_client_item_id); |
| |
| EXPECT_EQ(kGuid1, processor() |
| ->GetNthUpdateResponse(1) |
| .at(0) |
| ->entity.specifics.bookmark() |
| .guid()); |
| } |
| |
| TEST_F(ModelTypeWorkerBookmarksTest, |
| CanDecryptUpdateWithMissingBookmarkGUIDAndInvalidOCII) { |
| const std::string kInvalidOCII = "INVALID OCII"; |
| |
| // Initialize the worker with basic encryption state. |
| NormalInitialize(); |
| AddPendingKey(); |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // Generate specifics without a GUID and with an invalid |
| // originator_client_item_id. |
| sync_pb::SyncEntity entity; |
| entity.mutable_specifics()->mutable_bookmark()->set_url("www.foo.com"); |
| entity.mutable_specifics() |
| ->mutable_bookmark() |
| ->set_legacy_canonicalized_title("Title"); |
| entity.set_id_string("testserverid"); |
| entity.set_originator_client_item_id(kInvalidOCII); |
| *entity.mutable_unique_position() = |
| UniquePosition::InitialPosition(UniquePosition::RandomSuffix()).ToProto(); |
| |
| // Encrypt it. |
| EncryptUpdateWithNthKey(1, entity.mutable_specifics()); |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| DecryptPendingKey(); |
| |
| worker()->ProcessGetUpdatesResponse(server()->GetProgress(), |
| server()->GetContext(), {&entity}, |
| status_controller()); |
| worker()->ApplyUpdates(status_controller()); |
| |
| EXPECT_EQ(2U, processor()->GetNumUpdateResponses()); |
| |
| // First response should contain no updates, since ApplyUpdates() was called |
| // from within DecryptPendingKey() before any were added. |
| EXPECT_EQ(0U, processor()->GetNthUpdateResponse(0).size()); |
| EXPECT_EQ(1U, processor()->GetNthUpdateResponse(1).size()); |
| |
| EXPECT_EQ(kInvalidOCII, processor() |
| ->GetNthUpdateResponse(1) |
| .at(0) |
| ->entity.originator_client_item_id); |
| |
| EXPECT_TRUE(base::IsValidGUIDOutputString(processor() |
| ->GetNthUpdateResponse(1) |
| .at(0) |
| ->entity.specifics.bookmark() |
| .guid())); |
| } |
| |
| TEST_F(ModelTypeWorkerBookmarksTest, |
| CannotDecryptUpdateWithMissingBookmarkGUID) { |
| const std::string kGuid1 = base::GenerateGUID(); |
| |
| // Initialize the worker with basic encryption state. |
| NormalInitialize(); |
| AddPendingKey(); |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // Generate specifics without a GUID. |
| sync_pb::SyncEntity entity; |
| entity.mutable_specifics()->mutable_bookmark(); |
| entity.set_id_string("testserverid"); |
| entity.set_originator_client_item_id(kGuid1); |
| *entity.mutable_unique_position() = |
| UniquePosition::InitialPosition(UniquePosition::RandomSuffix()).ToProto(); |
| |
| // Encrypt it. |
| EncryptUpdateWithNthKey(1, entity.mutable_specifics()); |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| worker()->ProcessGetUpdatesResponse(server()->GetProgress(), |
| server()->GetContext(), {&entity}, |
| status_controller()); |
| worker()->ApplyUpdates(status_controller()); |
| |
| DecryptPendingKey(); |
| EXPECT_EQ(1U, processor()->GetNumUpdateResponses()); |
| |
| EXPECT_EQ(kGuid1, processor() |
| ->GetNthUpdateResponse(0) |
| .at(0) |
| ->entity.originator_client_item_id); |
| |
| EXPECT_EQ(kGuid1, processor() |
| ->GetNthUpdateResponse(0) |
| .at(0) |
| ->entity.specifics.bookmark() |
| .guid()); |
| } |
| |
| TEST_F(ModelTypeWorkerBookmarksTest, |
| CannotDecryptUpdateWithMissingBookmarkGUIDAndInvalidOCII) { |
| const std::string kInvalidOCII = "INVALID OCII"; |
| |
| // Initialize the worker with basic encryption state. |
| NormalInitialize(); |
| AddPendingKey(); |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| // Generate specifics without a GUID and with an invalid |
| // originator_client_item_id. |
| sync_pb::SyncEntity entity; |
| entity.mutable_specifics()->mutable_bookmark(); |
| entity.set_id_string("testserverid"); |
| entity.set_originator_client_item_id(kInvalidOCII); |
| *entity.mutable_unique_position() = |
| UniquePosition::InitialPosition(UniquePosition::RandomSuffix()).ToProto(); |
| |
| // Encrypt it. |
| EncryptUpdateWithNthKey(1, entity.mutable_specifics()); |
| EXPECT_EQ(0U, processor()->GetNumUpdateResponses()); |
| |
| worker()->ProcessGetUpdatesResponse(server()->GetProgress(), |
| server()->GetContext(), {&entity}, |
| status_controller()); |
| worker()->ApplyUpdates(status_controller()); |
| |
| DecryptPendingKey(); |
| EXPECT_EQ(1U, processor()->GetNumUpdateResponses()); |
| |
| EXPECT_EQ(kInvalidOCII, processor() |
| ->GetNthUpdateResponse(0) |
| .at(0) |
| ->entity.originator_client_item_id); |
| |
| EXPECT_TRUE(base::IsValidGUIDOutputString(processor() |
| ->GetNthUpdateResponse(0) |
| .at(0) |
| ->entity.specifics.bookmark() |
| .guid())); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, ShouldNotHaveLocalChangesOnSuccessfulLastCommit) { |
| const size_t kMaxEntities = 5; |
| |
| NormalInitialize(); |
| |
| ASSERT_FALSE(worker()->HasLocalChangesForTest()); |
| processor()->SetCommitRequest(GenerateCommitRequest(kTag1, kValue1)); |
| worker()->NudgeForCommit(); |
| ASSERT_TRUE(worker()->HasLocalChangesForTest()); |
| |
| std::unique_ptr<CommitContribution> contribution( |
| worker()->GetContribution(kMaxEntities)); |
| ASSERT_THAT(contribution, NotNull()); |
| ASSERT_EQ(1u, contribution->GetNumEntries()); |
| |
| // Entities are in-flight and it's considered to have local changes. |
| EXPECT_TRUE(worker()->HasLocalChangesForTest()); |
| |
| // Finish the commit successfully. |
| DoSuccessfulCommit(std::move(contribution)); |
| EXPECT_FALSE(worker()->HasLocalChangesForTest()); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, ShouldHaveLocalChangesOnCommitFailure) { |
| NormalInitialize(); |
| |
| ASSERT_FALSE(worker()->HasLocalChangesForTest()); |
| processor()->SetCommitRequest(GenerateCommitRequest(kTag1, kValue1)); |
| worker()->NudgeForCommit(); |
| ASSERT_TRUE(worker()->HasLocalChangesForTest()); |
| |
| DoCommitFailure(); |
| EXPECT_TRUE(worker()->HasLocalChangesForTest()); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, ShouldHaveLocalChangesOnSuccessfulNotLastCommit) { |
| const size_t kMaxEntities = 2; |
| NormalInitialize(); |
| |
| sync_pb::EntitySpecifics specifics; |
| specifics.mutable_bookmark(); |
| |
| ASSERT_FALSE(worker()->HasLocalChangesForTest()); |
| processor()->AppendCommitRequest(kHash1, specifics); |
| processor()->AppendCommitRequest(kHash2, specifics); |
| processor()->AppendCommitRequest(kHash3, specifics); |
| worker()->NudgeForCommit(); |
| ASSERT_TRUE(worker()->HasLocalChangesForTest()); |
| |
| std::unique_ptr<CommitContribution> contribution( |
| worker()->GetContribution(kMaxEntities)); |
| ASSERT_THAT(contribution, NotNull()); |
| ASSERT_EQ(kMaxEntities, contribution->GetNumEntries()); |
| DoSuccessfulCommit(std::move(contribution)); |
| |
| // There are still changes in the processor waiting for commit. |
| EXPECT_TRUE(worker()->HasLocalChangesForTest()); |
| |
| // Commit the rest of entities. |
| DoSuccessfulCommit(); |
| EXPECT_FALSE(worker()->HasLocalChangesForTest()); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, ShouldHaveLocalChangesWhenNudgedWhileInFlight) { |
| const size_t kMaxEntities = 5; |
| NormalInitialize(); |
| |
| ASSERT_FALSE(worker()->HasLocalChangesForTest()); |
| processor()->SetCommitRequest(GenerateCommitRequest(kTag1, kValue1)); |
| worker()->NudgeForCommit(); |
| ASSERT_TRUE(worker()->HasLocalChangesForTest()); |
| |
| // Start a commit. |
| std::unique_ptr<CommitContribution> contribution( |
| worker()->GetContribution(kMaxEntities)); |
| ASSERT_THAT(contribution, NotNull()); |
| ASSERT_EQ(1u, contribution->GetNumEntries()); |
| |
| // Add new data while the commit is in progress. |
| processor()->SetCommitRequest(GenerateCommitRequest(kTag2, kValue2)); |
| worker()->NudgeForCommit(); |
| EXPECT_TRUE(worker()->HasLocalChangesForTest()); |
| |
| // Finish the started commit request. |
| DoSuccessfulCommit(std::move(contribution)); |
| |
| // There are still entities to commit. |
| EXPECT_TRUE(worker()->HasLocalChangesForTest()); |
| |
| // Commit the rest of entities. |
| DoSuccessfulCommit(); |
| EXPECT_FALSE(worker()->HasLocalChangesForTest()); |
| } |
| |
| TEST_F(ModelTypeWorkerTest, ShouldHaveLocalChangesWhenContributedMaxEntities) { |
| const size_t kMaxEntities = 2; |
| NormalInitialize(); |
| ASSERT_FALSE(worker()->HasLocalChangesForTest()); |
| |
| processor()->AppendCommitRequest(kHash1, GenerateSpecifics(kTag1, kValue1)); |
| processor()->AppendCommitRequest(kHash2, GenerateSpecifics(kTag2, kValue2)); |
| worker()->NudgeForCommit(); |
| ASSERT_TRUE(worker()->HasLocalChangesForTest()); |
| |
| std::unique_ptr<CommitContribution> contribution( |
| worker()->GetContribution(kMaxEntities)); |
| ASSERT_THAT(contribution, NotNull()); |
| ASSERT_EQ(kMaxEntities, contribution->GetNumEntries()); |
| DoSuccessfulCommit(std::move(contribution)); |
| |
| // The worker is still not aware if there are more changes available. It is |
| // supposed that GetContribution() will be called until it returns less than |
| // |max_entities| items. This is not the intended behaviour, but this is how |
| // things currently work. |
| EXPECT_TRUE(worker()->HasLocalChangesForTest()); |
| contribution = worker()->GetContribution(kMaxEntities); |
| ASSERT_THAT(contribution, IsNull()); |
| EXPECT_FALSE(worker()->HasLocalChangesForTest()); |
| } |
| |
| } // namespace syncer |