// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "sync/engine/model_type_sync_proxy_impl.h" #include "sync/engine/model_type_sync_worker.h" #include "sync/internal_api/public/base/model_type.h" #include "sync/internal_api/public/non_blocking_sync_common.h" #include "sync/internal_api/public/sync_context_proxy.h" #include "sync/protocol/sync.pb.h" #include "sync/syncable/syncable_util.h" #include "sync/test/engine/injectable_sync_context_proxy.h" #include "sync/test/engine/mock_model_type_sync_worker.h" #include "testing/gtest/include/gtest/gtest.h" namespace syncer_v2 { static const syncer::ModelType kModelType = syncer::PREFERENCES; // Tests the sync engine parts of ModelTypeSyncProxyImpl. // // The ModelTypeSyncProxyImpl contains a non-trivial amount of code dedicated // to turning the sync engine on and off again. That code is fairly well // tested in the NonBlockingDataTypeController unit tests and it doesn't need // to be re-tested here. // // These tests skip past initialization and focus on steady state sync engine // behvior. This is where we test how the type sync proxy responds to the // model's requests to make changes to its data, the messages incoming from the // sync server, and what happens when the two conflict. // // Inputs: // - Initial state from permanent storage. (TODO) // - Create, update or delete requests from the model. // - Update responses and commit responses from the server. // // Outputs: // - Writes to permanent storage. (TODO) // - Callbacks into the model. (TODO) // - Requests to the sync thread. Tested with MockModelTypeSyncWorker. class ModelTypeSyncProxyImplTest : public ::testing::Test { public: ModelTypeSyncProxyImplTest(); ~ModelTypeSyncProxyImplTest() override; // Initialize with no local state. The type sync proxy will be unable to // commit until it receives notification that initial sync has completed. void FirstTimeInitialize(); // Initialize to a "ready-to-commit" state. void InitializeToReadyState(); // Disconnect the ModelTypeSyncWorker from our ModelTypeSyncProxyImpl. void Disconnect(); // Disable sync for this ModelTypeSyncProxyImpl. Should cause sync state to // be discarded. void Disable(); // Re-enable sync after Disconnect() or Disable(). void ReEnable(); // Local data modification. Emulates signals from the model thread. void WriteItem(const std::string& tag, const std::string& value); void DeleteItem(const std::string& tag); // Emulates an "initial sync done" message from the // ModelTypeSyncWorker. void OnInitialSyncDone(); // Emulate updates from the server. // This harness has some functionality to help emulate server behavior. // See the definitions of these methods for more information. void UpdateFromServer(int64 version_offset, const std::string& tag, const std::string& value); void TombstoneFromServer(int64 version_offset, const std::string& tag); // Emulate the receipt of pending updates from the server. // Pending updates are usually caused by a temporary decryption failure. void PendingUpdateFromServer(int64 version_offset, const std::string& tag, const std::string& value, const std::string& key_name); // Returns true if the proxy has an pending update with specified tag. bool HasPendingUpdate(const std::string& tag) const; // Returns the pending update with the specified tag. UpdateResponseData GetPendingUpdate(const std::string& tag) const; // Returns the number of pending updates. size_t GetNumPendingUpdates() const; // Read emitted commit requests as batches. size_t GetNumCommitRequestLists(); CommitRequestDataList GetNthCommitRequestList(size_t n); // Read emitted commit requests by tag, most recent only. bool HasCommitRequestForTag(const std::string& tag); CommitRequestData GetLatestCommitRequestForTag(const std::string& tag); // Sends the type sync proxy a successful commit response. void SuccessfulCommitResponse(const CommitRequestData& request_data); // Sends the type sync proxy an updated DataTypeState to let it know that // the desired encryption key has changed. void UpdateDesiredEncryptionKey(const std::string& key_name); // Sets the key_name that the mock ModelTypeSyncWorker will claim is in use // when receiving items. void SetServerEncryptionKey(const std::string& key_name); private: static std::string GenerateTagHash(const std::string& tag); static sync_pb::EntitySpecifics GenerateSpecifics(const std::string& tag, const std::string& value); static sync_pb::EntitySpecifics GenerateEncryptedSpecifics( const std::string& tag, const std::string& value, const std::string& key_name); int64 GetServerVersion(const std::string& tag); void SetServerVersion(const std::string& tag, int64 version); MockModelTypeSyncWorker* mock_worker_; scoped_ptr injectable_sync_context_proxy_; scoped_ptr type_sync_proxy_; DataTypeState data_type_state_; }; ModelTypeSyncProxyImplTest::ModelTypeSyncProxyImplTest() : mock_worker_(new MockModelTypeSyncWorker()), injectable_sync_context_proxy_( new InjectableSyncContextProxy(mock_worker_)), type_sync_proxy_(new ModelTypeSyncProxyImpl(kModelType)) { } ModelTypeSyncProxyImplTest::~ModelTypeSyncProxyImplTest() { } void ModelTypeSyncProxyImplTest::FirstTimeInitialize() { type_sync_proxy_->Enable(injectable_sync_context_proxy_->Clone()); } void ModelTypeSyncProxyImplTest::InitializeToReadyState() { // TODO(rlarocque): This should be updated to inject on-disk state. // At the time this code was written, there was no support for on-disk // state so this was the only way to inject a data_type_state into // the |type_sync_proxy_|. FirstTimeInitialize(); OnInitialSyncDone(); } void ModelTypeSyncProxyImplTest::Disconnect() { type_sync_proxy_->Disconnect(); injectable_sync_context_proxy_.reset(); mock_worker_ = NULL; } void ModelTypeSyncProxyImplTest::Disable() { type_sync_proxy_->Disable(); injectable_sync_context_proxy_.reset(); mock_worker_ = NULL; } void ModelTypeSyncProxyImplTest::ReEnable() { DCHECK(!type_sync_proxy_->IsConnected()); // Prepare a new MockModelTypeSyncWorker instance, just as we would // if this happened in the real world. mock_worker_ = new MockModelTypeSyncWorker(); injectable_sync_context_proxy_.reset( new InjectableSyncContextProxy(mock_worker_)); // Re-enable sync with the new ModelTypeSyncWorker. type_sync_proxy_->Enable(injectable_sync_context_proxy_->Clone()); } void ModelTypeSyncProxyImplTest::WriteItem(const std::string& tag, const std::string& value) { const std::string tag_hash = GenerateTagHash(tag); type_sync_proxy_->Put(tag, GenerateSpecifics(tag, value)); } void ModelTypeSyncProxyImplTest::DeleteItem(const std::string& tag) { type_sync_proxy_->Delete(tag); } void ModelTypeSyncProxyImplTest::OnInitialSyncDone() { data_type_state_.initial_sync_done = true; UpdateResponseDataList empty_update_list; type_sync_proxy_->OnUpdateReceived( data_type_state_, empty_update_list, empty_update_list); } void ModelTypeSyncProxyImplTest::UpdateFromServer(int64 version_offset, const std::string& tag, const std::string& value) { const std::string tag_hash = GenerateTagHash(tag); UpdateResponseData data = mock_worker_->UpdateFromServer( version_offset, tag_hash, GenerateSpecifics(tag, value)); UpdateResponseDataList list; list.push_back(data); type_sync_proxy_->OnUpdateReceived(data_type_state_, list, UpdateResponseDataList()); } void ModelTypeSyncProxyImplTest::PendingUpdateFromServer( int64 version_offset, const std::string& tag, const std::string& value, const std::string& key_name) { const std::string tag_hash = GenerateTagHash(tag); UpdateResponseData data = mock_worker_->UpdateFromServer( version_offset, tag_hash, GenerateEncryptedSpecifics(tag, value, key_name)); UpdateResponseDataList list; list.push_back(data); type_sync_proxy_->OnUpdateReceived(data_type_state_, UpdateResponseDataList(), list); } void ModelTypeSyncProxyImplTest::TombstoneFromServer(int64 version_offset, const std::string& tag) { // Overwrite the existing server version if this is the new highest version. std::string tag_hash = GenerateTagHash(tag); UpdateResponseData data = mock_worker_->TombstoneFromServer(version_offset, tag_hash); UpdateResponseDataList list; list.push_back(data); type_sync_proxy_->OnUpdateReceived(data_type_state_, list, UpdateResponseDataList()); } bool ModelTypeSyncProxyImplTest::HasPendingUpdate( const std::string& tag) const { const std::string client_tag_hash = GenerateTagHash(tag); const UpdateResponseDataList list = type_sync_proxy_->GetPendingUpdates(); for (UpdateResponseDataList::const_iterator it = list.begin(); it != list.end(); ++it) { if (it->client_tag_hash == client_tag_hash) return true; } return false; } UpdateResponseData ModelTypeSyncProxyImplTest::GetPendingUpdate( const std::string& tag) const { DCHECK(HasPendingUpdate(tag)); const std::string client_tag_hash = GenerateTagHash(tag); const UpdateResponseDataList list = type_sync_proxy_->GetPendingUpdates(); for (UpdateResponseDataList::const_iterator it = list.begin(); it != list.end(); ++it) { if (it->client_tag_hash == client_tag_hash) return *it; } NOTREACHED(); return UpdateResponseData(); } size_t ModelTypeSyncProxyImplTest::GetNumPendingUpdates() const { return type_sync_proxy_->GetPendingUpdates().size(); } void ModelTypeSyncProxyImplTest::SuccessfulCommitResponse( const CommitRequestData& request_data) { CommitResponseDataList list; list.push_back(mock_worker_->SuccessfulCommitResponse(request_data)); type_sync_proxy_->OnCommitCompleted(data_type_state_, list); } void ModelTypeSyncProxyImplTest::UpdateDesiredEncryptionKey( const std::string& key_name) { data_type_state_.encryption_key_name = key_name; type_sync_proxy_->OnUpdateReceived(data_type_state_, UpdateResponseDataList(), UpdateResponseDataList()); } void ModelTypeSyncProxyImplTest::SetServerEncryptionKey( const std::string& key_name) { mock_worker_->SetServerEncryptionKey(key_name); } std::string ModelTypeSyncProxyImplTest::GenerateTagHash( const std::string& tag) { return syncer::syncable::GenerateSyncableHash(kModelType, tag); } sync_pb::EntitySpecifics ModelTypeSyncProxyImplTest::GenerateSpecifics( const std::string& tag, const std::string& value) { sync_pb::EntitySpecifics specifics; specifics.mutable_preference()->set_name(tag); specifics.mutable_preference()->set_value(value); return specifics; } // These tests never decrypt anything, so we can get away with faking the // encryption for now. sync_pb::EntitySpecifics ModelTypeSyncProxyImplTest::GenerateEncryptedSpecifics( const std::string& tag, const std::string& value, const std::string& key_name) { sync_pb::EntitySpecifics specifics; syncer::AddDefaultFieldValue(kModelType, &specifics); specifics.mutable_encrypted()->set_key_name(key_name); specifics.mutable_encrypted()->set_blob("BLOB" + key_name); return specifics; } size_t ModelTypeSyncProxyImplTest::GetNumCommitRequestLists() { return mock_worker_->GetNumCommitRequestLists(); } CommitRequestDataList ModelTypeSyncProxyImplTest::GetNthCommitRequestList( size_t n) { return mock_worker_->GetNthCommitRequestList(n); } bool ModelTypeSyncProxyImplTest::HasCommitRequestForTag( const std::string& tag) { const std::string tag_hash = GenerateTagHash(tag); return mock_worker_->HasCommitRequestForTagHash(tag_hash); } CommitRequestData ModelTypeSyncProxyImplTest::GetLatestCommitRequestForTag( const std::string& tag) { const std::string tag_hash = GenerateTagHash(tag); return mock_worker_->GetLatestCommitRequestForTagHash(tag_hash); } // Creates a new item locally. // Thoroughly tests the data generated by a local item creation. TEST_F(ModelTypeSyncProxyImplTest, CreateLocalItem) { InitializeToReadyState(); EXPECT_EQ(0U, GetNumCommitRequestLists()); WriteItem("tag1", "value1"); // Verify the commit request this operation has triggered. EXPECT_EQ(1U, GetNumCommitRequestLists()); ASSERT_TRUE(HasCommitRequestForTag("tag1")); const CommitRequestData& tag1_data = GetLatestCommitRequestForTag("tag1"); EXPECT_TRUE(tag1_data.id.empty()); EXPECT_EQ(kUncommittedVersion, tag1_data.base_version); EXPECT_FALSE(tag1_data.ctime.is_null()); EXPECT_FALSE(tag1_data.mtime.is_null()); EXPECT_EQ("tag1", tag1_data.non_unique_name); EXPECT_FALSE(tag1_data.deleted); EXPECT_EQ("tag1", tag1_data.specifics.preference().name()); EXPECT_EQ("value1", tag1_data.specifics.preference().value()); } // Creates a new local item then modifies it. // Thoroughly tests data generated by modification of server-unknown item. TEST_F(ModelTypeSyncProxyImplTest, CreateAndModifyLocalItem) { InitializeToReadyState(); EXPECT_EQ(0U, GetNumCommitRequestLists()); WriteItem("tag1", "value1"); EXPECT_EQ(1U, GetNumCommitRequestLists()); ASSERT_TRUE(HasCommitRequestForTag("tag1")); const CommitRequestData& tag1_v1_data = GetLatestCommitRequestForTag("tag1"); WriteItem("tag1", "value2"); EXPECT_EQ(2U, GetNumCommitRequestLists()); ASSERT_TRUE(HasCommitRequestForTag("tag1")); const CommitRequestData& tag1_v2_data = GetLatestCommitRequestForTag("tag1"); // Test some of the relations between old and new commit requests. EXPECT_EQ(tag1_v1_data.specifics.preference().value(), "value1"); EXPECT_GT(tag1_v2_data.sequence_number, tag1_v1_data.sequence_number); // Perform a thorough examination of the update-generated request. EXPECT_TRUE(tag1_v2_data.id.empty()); EXPECT_EQ(kUncommittedVersion, tag1_v2_data.base_version); EXPECT_FALSE(tag1_v2_data.ctime.is_null()); EXPECT_FALSE(tag1_v2_data.mtime.is_null()); EXPECT_EQ("tag1", tag1_v2_data.non_unique_name); EXPECT_FALSE(tag1_v2_data.deleted); EXPECT_EQ("tag1", tag1_v2_data.specifics.preference().name()); EXPECT_EQ("value2", tag1_v2_data.specifics.preference().value()); } // Deletes an item we've never seen before. // Should have no effect and not crash. TEST_F(ModelTypeSyncProxyImplTest, DeleteUnknown) { InitializeToReadyState(); DeleteItem("tag1"); EXPECT_EQ(0U, GetNumCommitRequestLists()); } // Creates an item locally then deletes it. // // In this test, no commit responses are received, so the deleted item is // server-unknown as far as the model thread is concerned. That behavior // is race-dependent; other tests are used to test other races. TEST_F(ModelTypeSyncProxyImplTest, DeleteServerUnknown) { InitializeToReadyState(); WriteItem("tag1", "value1"); EXPECT_EQ(1U, GetNumCommitRequestLists()); ASSERT_TRUE(HasCommitRequestForTag("tag1")); const CommitRequestData& tag1_v1_data = GetLatestCommitRequestForTag("tag1"); DeleteItem("tag1"); EXPECT_EQ(2U, GetNumCommitRequestLists()); ASSERT_TRUE(HasCommitRequestForTag("tag1")); const CommitRequestData& tag1_v2_data = GetLatestCommitRequestForTag("tag1"); EXPECT_GT(tag1_v2_data.sequence_number, tag1_v1_data.sequence_number); EXPECT_TRUE(tag1_v2_data.id.empty()); EXPECT_EQ(kUncommittedVersion, tag1_v2_data.base_version); EXPECT_TRUE(tag1_v2_data.deleted); } // Creates an item locally then deletes it. // // The item is created locally then enqueued for commit. The sync thread // successfully commits it, but, before the commit response is picked up // by the model thread, the item is deleted by the model thread. TEST_F(ModelTypeSyncProxyImplTest, DeleteServerUnknown_RacyCommitResponse) { InitializeToReadyState(); WriteItem("tag1", "value1"); EXPECT_EQ(1U, GetNumCommitRequestLists()); ASSERT_TRUE(HasCommitRequestForTag("tag1")); const CommitRequestData& tag1_v1_data = GetLatestCommitRequestForTag("tag1"); DeleteItem("tag1"); EXPECT_EQ(2U, GetNumCommitRequestLists()); ASSERT_TRUE(HasCommitRequestForTag("tag1")); // This commit happened while the deletion was in progress, but the commit // response didn't arrive on our thread until after the delete was issued to // the sync thread. It will update some metadata, but won't do much else. SuccessfulCommitResponse(tag1_v1_data); // TODO(rlarocque): Verify the state of the item is correct once we get // storage hooked up in these tests. For example, verify the item is still // marked as deleted. } // Creates two different sync items. // Verifies that the second has no effect on the first. TEST_F(ModelTypeSyncProxyImplTest, TwoIndependentItems) { InitializeToReadyState(); EXPECT_EQ(0U, GetNumCommitRequestLists()); WriteItem("tag1", "value1"); // There should be one commit request for this item only. ASSERT_EQ(1U, GetNumCommitRequestLists()); EXPECT_EQ(1U, GetNthCommitRequestList(0).size()); ASSERT_TRUE(HasCommitRequestForTag("tag1")); WriteItem("tag2", "value2"); // The second write should trigger another single-item commit request. ASSERT_EQ(2U, GetNumCommitRequestLists()); EXPECT_EQ(1U, GetNthCommitRequestList(1).size()); ASSERT_TRUE(HasCommitRequestForTag("tag2")); } // Starts the type sync proxy with no local state. // Verify that it waits until initial sync is complete before requesting // commits. TEST_F(ModelTypeSyncProxyImplTest, NoCommitsUntilInitialSyncDone) { FirstTimeInitialize(); WriteItem("tag1", "value1"); EXPECT_EQ(0U, GetNumCommitRequestLists()); OnInitialSyncDone(); EXPECT_EQ(1U, GetNumCommitRequestLists()); EXPECT_TRUE(HasCommitRequestForTag("tag1")); } // Test proper handling of disconnect and reconnect. // // Creates items in various states of commit and verifies they re-attempt to // commit on reconnect. TEST_F(ModelTypeSyncProxyImplTest, Disconnect) { InitializeToReadyState(); // The first item is fully committed. WriteItem("tag1", "value1"); ASSERT_TRUE(HasCommitRequestForTag("tag1")); SuccessfulCommitResponse(GetLatestCommitRequestForTag("tag1")); // The second item has a commit request in progress. WriteItem("tag2", "value2"); EXPECT_TRUE(HasCommitRequestForTag("tag2")); Disconnect(); // The third item is added after disconnection. WriteItem("tag3", "value3"); ReEnable(); EXPECT_EQ(1U, GetNumCommitRequestLists()); EXPECT_EQ(2U, GetNthCommitRequestList(0).size()); // The first item was already in sync. EXPECT_FALSE(HasCommitRequestForTag("tag1")); // The second item's commit was interrupted and should be retried. EXPECT_TRUE(HasCommitRequestForTag("tag2")); // The third item's commit was not started until the reconnect. EXPECT_TRUE(HasCommitRequestForTag("tag3")); } // Test proper handling of disable and re-enable. // // Creates items in various states of commit and verifies they re-attempt to // commit on re-enable. TEST_F(ModelTypeSyncProxyImplTest, Disable) { InitializeToReadyState(); // The first item is fully committed. WriteItem("tag1", "value1"); ASSERT_TRUE(HasCommitRequestForTag("tag1")); SuccessfulCommitResponse(GetLatestCommitRequestForTag("tag1")); // The second item has a commit request in progress. WriteItem("tag2", "value2"); EXPECT_TRUE(HasCommitRequestForTag("tag2")); Disable(); // The third item is added after disable. WriteItem("tag3", "value3"); // Now we re-enable. ReEnable(); // There should be nothing to commit right away, since we need to // re-initialize the client state first. EXPECT_EQ(0U, GetNumCommitRequestLists()); // Once we're ready to commit, all three local items should consider // themselves uncommitted and pending for commit. OnInitialSyncDone(); EXPECT_EQ(1U, GetNumCommitRequestLists()); EXPECT_EQ(3U, GetNthCommitRequestList(0).size()); EXPECT_TRUE(HasCommitRequestForTag("tag1")); EXPECT_TRUE(HasCommitRequestForTag("tag2")); EXPECT_TRUE(HasCommitRequestForTag("tag3")); } // Test receipt of pending updates. TEST_F(ModelTypeSyncProxyImplTest, ReceivePendingUpdates) { InitializeToReadyState(); EXPECT_FALSE(HasPendingUpdate("tag1")); EXPECT_EQ(0U, GetNumPendingUpdates()); // Receive a pending update. PendingUpdateFromServer(5, "tag1", "value1", "key1"); EXPECT_EQ(1U, GetNumPendingUpdates()); ASSERT_TRUE(HasPendingUpdate("tag1")); UpdateResponseData data1 = GetPendingUpdate("tag1"); EXPECT_EQ(5, data1.response_version); // Receive an updated version of a pending update. // It should overwrite the existing item. PendingUpdateFromServer(10, "tag1", "value15", "key1"); EXPECT_EQ(1U, GetNumPendingUpdates()); ASSERT_TRUE(HasPendingUpdate("tag1")); UpdateResponseData data2 = GetPendingUpdate("tag1"); EXPECT_EQ(15, data2.response_version); // Receive a stale version of a pending update. // It should have no effect. PendingUpdateFromServer(-3, "tag1", "value12", "key1"); EXPECT_EQ(1U, GetNumPendingUpdates()); ASSERT_TRUE(HasPendingUpdate("tag1")); UpdateResponseData data3 = GetPendingUpdate("tag1"); EXPECT_EQ(15, data3.response_version); } // Test that Disable clears pending update state. TEST_F(ModelTypeSyncProxyImplTest, DisableWithPendingUpdates) { InitializeToReadyState(); PendingUpdateFromServer(5, "tag1", "value1", "key1"); EXPECT_EQ(1U, GetNumPendingUpdates()); ASSERT_TRUE(HasPendingUpdate("tag1")); Disable(); ReEnable(); EXPECT_EQ(0U, GetNumPendingUpdates()); EXPECT_FALSE(HasPendingUpdate("tag1")); } // Test that Disconnect does not clear pending update state. TEST_F(ModelTypeSyncProxyImplTest, DisconnectWithPendingUpdates) { InitializeToReadyState(); PendingUpdateFromServer(5, "tag1", "value1", "key1"); EXPECT_EQ(1U, GetNumPendingUpdates()); ASSERT_TRUE(HasPendingUpdate("tag1")); Disconnect(); ReEnable(); EXPECT_EQ(1U, GetNumPendingUpdates()); EXPECT_TRUE(HasPendingUpdate("tag1")); } // Test re-encrypt everything when desired encryption key changes. TEST_F(ModelTypeSyncProxyImplTest, ReEncryptCommitsWithNewKey) { InitializeToReadyState(); // Commit an item. WriteItem("tag1", "value1"); ASSERT_TRUE(HasCommitRequestForTag("tag1")); const CommitRequestData& tag1_v1_data = GetLatestCommitRequestForTag("tag1"); SuccessfulCommitResponse(tag1_v1_data); // Create another item and don't wait for its commit response. WriteItem("tag2", "value2"); ASSERT_EQ(2U, GetNumCommitRequestLists()); // Receive notice that the account's desired encryption key has changed. UpdateDesiredEncryptionKey("k1"); // That should trigger a new commit request. ASSERT_EQ(3U, GetNumCommitRequestLists()); EXPECT_EQ(2U, GetNthCommitRequestList(2).size()); const CommitRequestData& tag1_enc = GetLatestCommitRequestForTag("tag1"); const CommitRequestData& tag2_enc = GetLatestCommitRequestForTag("tag2"); SuccessfulCommitResponse(tag1_enc); SuccessfulCommitResponse(tag2_enc); // And that should be the end of it. ASSERT_EQ(3U, GetNumCommitRequestLists()); } // Test receipt of updates with new and old keys. TEST_F(ModelTypeSyncProxyImplTest, ReEncryptUpdatesWithNewKey) { InitializeToReadyState(); // Receive an unencrpted update. UpdateFromServer(5, "no_enc", "value1"); ASSERT_EQ(0U, GetNumCommitRequestLists()); // Set desired encryption key to k2 to force updates to some items. UpdateDesiredEncryptionKey("k2"); ASSERT_EQ(1U, GetNumCommitRequestLists()); EXPECT_EQ(1U, GetNthCommitRequestList(0).size()); EXPECT_TRUE(HasCommitRequestForTag("no_enc")); // Receive an update that was encrypted with key k1. SetServerEncryptionKey("k1"); UpdateFromServer(10, "enc_k1", "value1"); // Receipt of updates encrypted with old key also forces a re-encrypt commit. ASSERT_EQ(2U, GetNumCommitRequestLists()); EXPECT_EQ(1U, GetNthCommitRequestList(1).size()); EXPECT_TRUE(HasCommitRequestForTag("enc_k1")); // Receive an update that was encrypted with key k2. SetServerEncryptionKey("k2"); UpdateFromServer(15, "enc_k2", "value1"); // That was the correct key, so no re-encryption is required. EXPECT_EQ(2U, GetNumCommitRequestLists()); EXPECT_FALSE(HasCommitRequestForTag("enc_k2")); } } // namespace syncer