Index: chrome/browser/sync/engine/apply_updates_command_unittest.cc |
diff --git a/chrome/browser/sync/engine/apply_updates_command_unittest.cc b/chrome/browser/sync/engine/apply_updates_command_unittest.cc |
index b3ca18c09369a1f21d72ea00ebeb9f36cc1aaeda..df1a1aec08c86008800f22d47f7fb093069521fc 100644 |
--- a/chrome/browser/sync/engine/apply_updates_command_unittest.cc |
+++ b/chrome/browser/sync/engine/apply_updates_command_unittest.cc |
@@ -2,13 +2,21 @@ |
// Use of this source code is governed by a BSD-style license that can be |
// found in the LICENSE file. |
+#include <string> |
+ |
+#include "base/format_macros.h" |
+#include "base/string_util.h" |
#include "chrome/browser/sync/engine/apply_updates_command.h" |
+#include "chrome/browser/sync/engine/syncer.h" |
+#include "chrome/browser/sync/engine/syncer_util.h" |
#include "chrome/browser/sync/protocol/bookmark_specifics.pb.h" |
#include "chrome/browser/sync/sessions/sync_session.h" |
#include "chrome/browser/sync/syncable/directory_manager.h" |
+#include "chrome/browser/sync/syncable/nigori_util.h" |
#include "chrome/browser/sync/syncable/syncable.h" |
#include "chrome/browser/sync/syncable/syncable_id.h" |
#include "chrome/test/sync/engine/syncer_command_test.h" |
+#include "chrome/test/sync/engine/test_id_factory.h" |
#include "testing/gtest/include/gtest/gtest.h" |
namespace browser_sync { |
@@ -16,6 +24,7 @@ namespace browser_sync { |
using sessions::SyncSession; |
using std::string; |
using syncable::Entry; |
+using syncable::GetEncryptedDataTypes; |
using syncable::Id; |
using syncable::MutableEntry; |
using syncable::ReadTransaction; |
@@ -41,7 +50,7 @@ class ApplyUpdatesCommandTest : public SyncerCommandTest { |
SyncerCommandTest::SetUp(); |
} |
- // Create a new unapplied update. |
+ // Create a new unapplied bookmark node with a parent. |
void CreateUnappliedNewItemWithParent(const string& item_id, |
const string& parent_id) { |
ScopedDirLookup dir(syncdb()->manager(), syncdb()->name()); |
@@ -61,8 +70,10 @@ class ApplyUpdatesCommandTest : public SyncerCommandTest { |
entry.Put(syncable::SERVER_SPECIFICS, default_bookmark_specifics); |
} |
+ // Create a new unapplied update without a parent. |
void CreateUnappliedNewItem(const string& item_id, |
- const sync_pb::EntitySpecifics& specifics) { |
+ const sync_pb::EntitySpecifics& specifics, |
+ bool is_unique) { |
ScopedDirLookup dir(syncdb()->manager(), syncdb()->name()); |
ASSERT_TRUE(dir.good()); |
WriteTransaction trans(dir, UNITTEST, __FILE__, __LINE__); |
@@ -71,15 +82,53 @@ class ApplyUpdatesCommandTest : public SyncerCommandTest { |
ASSERT_TRUE(entry.good()); |
entry.Put(syncable::SERVER_VERSION, next_revision_++); |
entry.Put(syncable::IS_UNAPPLIED_UPDATE, true); |
- |
entry.Put(syncable::SERVER_NON_UNIQUE_NAME, item_id); |
entry.Put(syncable::SERVER_PARENT_ID, syncable::kNullId); |
entry.Put(syncable::SERVER_IS_DIR, false); |
entry.Put(syncable::SERVER_SPECIFICS, specifics); |
+ if (is_unique) // For top-level nodes. |
+ entry.Put(syncable::UNIQUE_SERVER_TAG, item_id); |
} |
- ApplyUpdatesCommand apply_updates_command_; |
+ // Create an unsynced item in the database. If item_id is a local ID, it |
+ // will be treated as a create-new. Otherwise, if it's a server ID, we'll |
+ // fake the server data so that it looks like it exists on the server. |
+ // Returns the methandle of the created item in |metahandle_out| if not NULL. |
+ void CreateUnsyncedItem(const Id& item_id, |
+ const Id& parent_id, |
+ const string& name, |
+ bool is_folder, |
+ syncable::ModelType model_type, |
+ int64* metahandle_out) { |
+ ScopedDirLookup dir(syncdb()->manager(), syncdb()->name()); |
+ ASSERT_TRUE(dir.good()); |
+ WriteTransaction trans(dir, UNITTEST, __FILE__, __LINE__); |
+ Id predecessor_id = dir->GetLastChildId(&trans, parent_id); |
+ MutableEntry entry(&trans, syncable::CREATE, parent_id, name); |
+ ASSERT_TRUE(entry.good()); |
+ entry.Put(syncable::ID, item_id); |
+ entry.Put(syncable::BASE_VERSION, |
+ item_id.ServerKnows() ? next_revision_++ : 0); |
+ entry.Put(syncable::IS_UNSYNCED, true); |
+ entry.Put(syncable::IS_DIR, is_folder); |
+ entry.Put(syncable::IS_DEL, false); |
+ entry.Put(syncable::PARENT_ID, parent_id); |
+ entry.PutPredecessor(predecessor_id); |
+ sync_pb::EntitySpecifics default_specifics; |
+ syncable::AddDefaultExtensionValue(model_type, &default_specifics); |
+ entry.Put(syncable::SPECIFICS, default_specifics); |
+ if (item_id.ServerKnows()) { |
+ entry.Put(syncable::SERVER_SPECIFICS, default_specifics); |
+ entry.Put(syncable::SERVER_IS_DIR, is_folder); |
+ entry.Put(syncable::SERVER_PARENT_ID, parent_id); |
+ entry.Put(syncable::SERVER_IS_DEL, false); |
+ } |
+ if (metahandle_out) |
+ *metahandle_out = entry.Get(syncable::META_HANDLE); |
+ } |
+ ApplyUpdatesCommand apply_updates_command_; |
+ TestIdFactory id_factory_; |
private: |
int64 next_revision_; |
DISALLOW_COPY_AND_ASSIGN(ApplyUpdatesCommandTest); |
@@ -178,7 +227,7 @@ TEST_F(ApplyUpdatesCommandTest, DecryptablePassword) { |
cryptographer->Encrypt(data, |
specifics.MutableExtension(sync_pb::password)->mutable_encrypted()); |
- CreateUnappliedNewItem("item", specifics); |
+ CreateUnappliedNewItem("item", specifics, false); |
apply_updates_command_.ExecuteImpl(session()); |
@@ -196,7 +245,7 @@ TEST_F(ApplyUpdatesCommandTest, UndecryptablePassword) { |
// Undecryptable password updates should not be applied. |
sync_pb::EntitySpecifics specifics; |
specifics.MutableExtension(sync_pb::password); |
- CreateUnappliedNewItem("item", specifics); |
+ CreateUnappliedNewItem("item", specifics, false); |
apply_updates_command_.ExecuteImpl(session()); |
@@ -225,7 +274,7 @@ TEST_F(ApplyUpdatesCommandTest, SomeUndecryptablePassword) { |
cryptographer->Encrypt(data, |
specifics.MutableExtension(sync_pb::password)->mutable_encrypted()); |
- CreateUnappliedNewItem("item1", specifics); |
+ CreateUnappliedNewItem("item1", specifics, false); |
} |
{ |
// Create a new cryptographer, independent of the one in the session. |
@@ -239,7 +288,7 @@ TEST_F(ApplyUpdatesCommandTest, SomeUndecryptablePassword) { |
cryptographer.Encrypt(data, |
specifics.MutableExtension(sync_pb::password)->mutable_encrypted()); |
- CreateUnappliedNewItem("item2", specifics); |
+ CreateUnappliedNewItem("item2", specifics, false); |
} |
apply_updates_command_.ExecuteImpl(session()); |
@@ -255,20 +304,109 @@ TEST_F(ApplyUpdatesCommandTest, SomeUndecryptablePassword) { |
} |
TEST_F(ApplyUpdatesCommandTest, NigoriUpdate) { |
+ syncable::ModelTypeSet encrypted_types; |
+ { |
+ ScopedDirLookup dir(syncdb()->manager(), syncdb()->name()); |
+ ASSERT_TRUE(dir.good()); |
+ ReadTransaction trans(dir, __FILE__, __LINE__); |
+ EXPECT_EQ(encrypted_types, GetEncryptedDataTypes(&trans)); |
+ } |
+ |
// Nigori node updates should update the Cryptographer. |
Cryptographer other_cryptographer; |
KeyParams params = {"localhost", "dummy", "foobar"}; |
other_cryptographer.AddKey(params); |
sync_pb::EntitySpecifics specifics; |
- other_cryptographer.GetKeys( |
- specifics.MutableExtension(sync_pb::nigori)->mutable_encrypted()); |
+ sync_pb::NigoriSpecifics* nigori = |
+ specifics.MutableExtension(sync_pb::nigori); |
+ other_cryptographer.GetKeys(nigori->mutable_encrypted()); |
+ nigori->set_encrypt_bookmarks(true); |
+ encrypted_types.insert(syncable::BOOKMARKS); |
+ CreateUnappliedNewItem(syncable::ModelTypeToRootTag(syncable::NIGORI), |
+ specifics, true); |
+ |
+ Cryptographer* cryptographer = |
+ session()->context()->directory_manager()->cryptographer(); |
+ EXPECT_FALSE(cryptographer->has_pending_keys()); |
+ |
+ apply_updates_command_.ExecuteImpl(session()); |
+ |
+ sessions::StatusController* status = session()->status_controller(); |
+ sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); |
+ EXPECT_EQ(1, status->update_progress().AppliedUpdatesSize()) |
+ << "All updates should have been attempted"; |
+ EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize()) |
+ << "The nigori update shouldn't be in conflict"; |
+ EXPECT_EQ(1, status->update_progress().SuccessfullyAppliedUpdateCount()) |
+ << "The nigori update should be applied"; |
+ |
+ EXPECT_FALSE(cryptographer->is_ready()); |
+ EXPECT_TRUE(cryptographer->has_pending_keys()); |
+} |
+ |
+TEST_F(ApplyUpdatesCommandTest, EncryptUnsyncedChanges) { |
+ syncable::ModelTypeSet encrypted_types; |
+ { |
+ ScopedDirLookup dir(syncdb()->manager(), syncdb()->name()); |
+ ASSERT_TRUE(dir.good()); |
+ ReadTransaction trans(dir, __FILE__, __LINE__); |
+ EXPECT_EQ(encrypted_types, GetEncryptedDataTypes(&trans)); |
+ |
+ // With empty encrypted_types, this should be true. |
+ EXPECT_TRUE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types)); |
+ |
+ Syncer::UnsyncedMetaHandles handles; |
+ SyncerUtil::GetUnsyncedEntries(&trans, &handles); |
+ EXPECT_TRUE(handles.empty()); |
+ } |
- CreateUnappliedNewItem("item", specifics); |
+ // Create unsynced bookmarks without encryption. |
+ // First item is a folder |
+ Id folder_id = id_factory_.NewLocalId(); |
+ CreateUnsyncedItem(folder_id, id_factory_.root(), "folder", |
+ true, syncable::BOOKMARKS, NULL); |
+ // Next five items are children of the folder |
+ size_t i; |
+ size_t batch_s = 5; |
+ for (i = 0; i < batch_s; ++i) { |
+ CreateUnsyncedItem(id_factory_.NewLocalId(), folder_id, |
+ StringPrintf("Item %"PRIuS"", i), false, |
+ syncable::BOOKMARKS, NULL); |
+ } |
+ // Next five items are children of the root. |
+ for (; i < 2*batch_s; ++i) { |
+ CreateUnsyncedItem(id_factory_.NewLocalId(), id_factory_.root(), |
+ StringPrintf("Item %"PRIuS"", i), false, |
+ syncable::BOOKMARKS, NULL); |
+ } |
Cryptographer* cryptographer = |
session()->context()->directory_manager()->cryptographer(); |
+ KeyParams params = {"localhost", "dummy", "foobar"}; |
+ cryptographer->AddKey(params); |
+ sync_pb::EntitySpecifics specifics; |
+ sync_pb::NigoriSpecifics* nigori = |
+ specifics.MutableExtension(sync_pb::nigori); |
+ cryptographer->GetKeys(nigori->mutable_encrypted()); |
+ nigori->set_encrypt_bookmarks(true); |
+ encrypted_types.insert(syncable::BOOKMARKS); |
+ CreateUnappliedNewItem(syncable::ModelTypeToRootTag(syncable::NIGORI), |
+ specifics, true); |
EXPECT_FALSE(cryptographer->has_pending_keys()); |
+ EXPECT_TRUE(cryptographer->is_ready()); |
+ |
+ { |
+ // Ensure we have unsynced nodes that aren't properly encrypted. |
+ ScopedDirLookup dir(syncdb()->manager(), syncdb()->name()); |
+ ASSERT_TRUE(dir.good()); |
+ ReadTransaction trans(dir, __FILE__, __LINE__); |
+ EXPECT_FALSE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types)); |
+ |
+ Syncer::UnsyncedMetaHandles handles; |
+ SyncerUtil::GetUnsyncedEntries(&trans, &handles); |
+ EXPECT_EQ(2*batch_s+1, handles.size()); |
+ } |
apply_updates_command_.ExecuteImpl(session()); |
@@ -280,9 +418,116 @@ TEST_F(ApplyUpdatesCommandTest, NigoriUpdate) { |
<< "The nigori update shouldn't be in conflict"; |
EXPECT_EQ(1, status->update_progress().SuccessfullyAppliedUpdateCount()) |
<< "The nigori update should be applied"; |
+ EXPECT_FALSE(cryptographer->has_pending_keys()); |
+ EXPECT_TRUE(cryptographer->is_ready()); |
+ { |
+ ScopedDirLookup dir(syncdb()->manager(), syncdb()->name()); |
+ ASSERT_TRUE(dir.good()); |
+ ReadTransaction trans(dir, __FILE__, __LINE__); |
+ |
+ // If ProcessUnsyncedChangesForEncryption worked, all our unsynced changes |
+ // should be encrypted now. |
+ EXPECT_EQ(encrypted_types, GetEncryptedDataTypes(&trans)); |
+ EXPECT_TRUE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types)); |
+ |
+ Syncer::UnsyncedMetaHandles handles; |
+ SyncerUtil::GetUnsyncedEntries(&trans, &handles); |
+ EXPECT_EQ(2*batch_s+1, handles.size()); |
+ } |
+} |
+ |
+TEST_F(ApplyUpdatesCommandTest, CannotEncryptUnsyncedChanges) { |
+ syncable::ModelTypeSet encrypted_types; |
+ { |
+ ScopedDirLookup dir(syncdb()->manager(), syncdb()->name()); |
+ ASSERT_TRUE(dir.good()); |
+ ReadTransaction trans(dir, __FILE__, __LINE__); |
+ EXPECT_EQ(encrypted_types, GetEncryptedDataTypes(&trans)); |
+ |
+ // With empty encrypted_types, this should be true. |
+ EXPECT_TRUE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types)); |
+ |
+ Syncer::UnsyncedMetaHandles handles; |
+ SyncerUtil::GetUnsyncedEntries(&trans, &handles); |
+ EXPECT_TRUE(handles.empty()); |
+ } |
+ |
+ // Create unsynced bookmarks without encryption. |
+ // First item is a folder |
+ Id folder_id = id_factory_.NewLocalId(); |
+ CreateUnsyncedItem(folder_id, id_factory_.root(), "folder", true, |
+ syncable::BOOKMARKS, NULL); |
+ // Next five items are children of the folder |
+ size_t i; |
+ size_t batch_s = 5; |
+ for (i = 0; i < batch_s; ++i) { |
+ CreateUnsyncedItem(id_factory_.NewLocalId(), folder_id, |
+ StringPrintf("Item %"PRIuS"", i), false, |
+ syncable::BOOKMARKS, NULL); |
+ } |
+ // Next five items are children of the root. |
+ for (; i < 2*batch_s; ++i) { |
+ CreateUnsyncedItem(id_factory_.NewLocalId(), id_factory_.root(), |
+ StringPrintf("Item %"PRIuS"", i), false, |
+ syncable::BOOKMARKS, NULL); |
+ } |
+ |
+ // We encrypt with new keys, triggering the local cryptographer to be unready |
+ // and unable to decrypt data (once updated). |
+ Cryptographer other_cryptographer; |
+ KeyParams params = {"localhost", "dummy", "foobar"}; |
+ other_cryptographer.AddKey(params); |
+ sync_pb::EntitySpecifics specifics; |
+ sync_pb::NigoriSpecifics* nigori = |
+ specifics.MutableExtension(sync_pb::nigori); |
+ other_cryptographer.GetKeys(nigori->mutable_encrypted()); |
+ nigori->set_encrypt_bookmarks(true); |
+ encrypted_types.insert(syncable::BOOKMARKS); |
+ CreateUnappliedNewItem(syncable::ModelTypeToRootTag(syncable::NIGORI), |
+ specifics, true); |
+ Cryptographer* cryptographer = |
+ session()->context()->directory_manager()->cryptographer(); |
+ EXPECT_FALSE(cryptographer->has_pending_keys()); |
+ |
+ { |
+ // Ensure we have unsynced nodes that aren't properly encrypted. |
+ ScopedDirLookup dir(syncdb()->manager(), syncdb()->name()); |
+ ASSERT_TRUE(dir.good()); |
+ ReadTransaction trans(dir, __FILE__, __LINE__); |
+ EXPECT_FALSE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types)); |
+ Syncer::UnsyncedMetaHandles handles; |
+ SyncerUtil::GetUnsyncedEntries(&trans, &handles); |
+ EXPECT_EQ(2*batch_s+1, handles.size()); |
+ } |
+ |
+ apply_updates_command_.ExecuteImpl(session()); |
+ sessions::StatusController* status = session()->status_controller(); |
+ sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); |
+ EXPECT_EQ(1, status->update_progress().AppliedUpdatesSize()) |
+ << "All updates should have been attempted"; |
+ EXPECT_EQ(1, status->conflict_progress().ConflictingItemsSize()) |
+ << "The unsynced chnages trigger a conflict with the nigori update."; |
+ EXPECT_EQ(0, status->update_progress().SuccessfullyAppliedUpdateCount()) |
+ << "The nigori update should not be applied"; |
EXPECT_FALSE(cryptographer->is_ready()); |
EXPECT_TRUE(cryptographer->has_pending_keys()); |
+ { |
+ // Ensure the unsynced nodes are still not encrypted. |
+ ScopedDirLookup dir(syncdb()->manager(), syncdb()->name()); |
+ ASSERT_TRUE(dir.good()); |
+ ReadTransaction trans(dir, __FILE__, __LINE__); |
+ |
+ // Since we're in conflict, the specifics don't reflect the unapplied |
+ // changes. |
+ EXPECT_FALSE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types)); |
+ encrypted_types.clear(); |
+ EXPECT_EQ(encrypted_types, GetEncryptedDataTypes(&trans)); |
+ |
+ Syncer::UnsyncedMetaHandles handles; |
+ SyncerUtil::GetUnsyncedEntries(&trans, &handles); |
+ EXPECT_EQ(2*batch_s+1, handles.size()); |
+ } |
} |
} // namespace browser_sync |