blob: 852bfc02b55e491a8af6dbe6755b77e2fb170a38 [file] [log] [blame]
// Copyright (c) 2012 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 <string>
#include "base/format_macros.h"
#include "base/location.h"
#include "base/memory/scoped_ptr.h"
#include "base/stringprintf.h"
#include "sync/engine/apply_updates_command.h"
#include "sync/engine/syncer.h"
#include "sync/internal_api/public/test/test_entry_factory.h"
#include "sync/protocol/bookmark_specifics.pb.h"
#include "sync/protocol/password_specifics.pb.h"
#include "sync/sessions/sync_session.h"
#include "sync/syncable/mutable_entry.h"
#include "sync/syncable/nigori_util.h"
#include "sync/syncable/read_transaction.h"
#include "sync/syncable/syncable_id.h"
#include "sync/syncable/syncable_util.h"
#include "sync/syncable/write_transaction.h"
#include "sync/test/engine/fake_model_worker.h"
#include "sync/test/engine/syncer_command_test.h"
#include "sync/test/engine/test_id_factory.h"
#include "sync/test/fake_encryptor.h"
#include "sync/util/cryptographer.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace syncer {
using sessions::SyncSession;
using std::string;
using syncable::Id;
using syncable::MutableEntry;
using syncable::UNITTEST;
using syncable::WriteTransaction;
namespace {
sync_pb::EntitySpecifics DefaultBookmarkSpecifics() {
sync_pb::EntitySpecifics result;
AddDefaultFieldValue(BOOKMARKS, &result);
return result;
}
} // namespace
// A test fixture for tests exercising ApplyUpdatesCommand.
class ApplyUpdatesCommandTest : public SyncerCommandTest {
public:
protected:
ApplyUpdatesCommandTest() {}
virtual ~ApplyUpdatesCommandTest() {}
virtual void SetUp() {
workers()->clear();
mutable_routing_info()->clear();
workers()->push_back(
make_scoped_refptr(new FakeModelWorker(GROUP_UI)));
workers()->push_back(
make_scoped_refptr(new FakeModelWorker(GROUP_PASSWORD)));
(*mutable_routing_info())[BOOKMARKS] = GROUP_UI;
(*mutable_routing_info())[PASSWORDS] = GROUP_PASSWORD;
(*mutable_routing_info())[NIGORI] = GROUP_PASSIVE;
SyncerCommandTest::SetUp();
entry_factory_.reset(new TestEntryFactory(directory()));
ExpectNoGroupsToChange(apply_updates_command_);
}
ApplyUpdatesCommand apply_updates_command_;
FakeEncryptor encryptor_;
TestIdFactory id_factory_;
scoped_ptr<TestEntryFactory> entry_factory_;
private:
DISALLOW_COPY_AND_ASSIGN(ApplyUpdatesCommandTest);
};
TEST_F(ApplyUpdatesCommandTest, Simple) {
string root_server_id = syncable::GetNullId().GetServerId();
entry_factory_->CreateUnappliedNewItemWithParent("parent",
DefaultBookmarkSpecifics(),
root_server_id);
entry_factory_->CreateUnappliedNewItemWithParent("child",
DefaultBookmarkSpecifics(),
"parent");
ExpectGroupToChange(apply_updates_command_, GROUP_UI);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
ASSERT_TRUE(status->update_progress());
EXPECT_EQ(2, status->update_progress()->AppliedUpdatesSize())
<< "All updates should have been attempted";
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize())
<< "Simple update shouldn't result in conflicts";
EXPECT_EQ(0, status->conflict_progress()->EncryptionConflictingItemsSize())
<< "Simple update shouldn't result in conflicts";
EXPECT_EQ(0, status->conflict_progress()->HierarchyConflictingItemsSize())
<< "Simple update shouldn't result in conflicts";
EXPECT_EQ(2, status->update_progress()->SuccessfullyAppliedUpdateCount())
<< "All items should have been successfully applied";
}
TEST_F(ApplyUpdatesCommandTest, UpdateWithChildrenBeforeParents) {
// Set a bunch of updates which are difficult to apply in the order
// they're received due to dependencies on other unseen items.
string root_server_id = syncable::GetNullId().GetServerId();
entry_factory_->CreateUnappliedNewItemWithParent(
"a_child_created_first", DefaultBookmarkSpecifics(), "parent");
entry_factory_->CreateUnappliedNewItemWithParent(
"x_child_created_first", DefaultBookmarkSpecifics(), "parent");
entry_factory_->CreateUnappliedNewItemWithParent(
"parent", DefaultBookmarkSpecifics(), root_server_id);
entry_factory_->CreateUnappliedNewItemWithParent(
"a_child_created_second", DefaultBookmarkSpecifics(), "parent");
entry_factory_->CreateUnappliedNewItemWithParent(
"x_child_created_second", DefaultBookmarkSpecifics(), "parent");
ExpectGroupToChange(apply_updates_command_, GROUP_UI);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
ASSERT_TRUE(status->update_progress());
EXPECT_EQ(5, status->update_progress()->AppliedUpdatesSize())
<< "All updates should have been attempted";
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize())
<< "Simple update shouldn't result in conflicts, even if out-of-order";
EXPECT_EQ(5, status->update_progress()->SuccessfullyAppliedUpdateCount())
<< "All updates should have been successfully applied";
}
// Runs the ApplyUpdatesCommand on an item that has both local and remote
// modifications (IS_UNSYNCED and IS_UNAPPLIED_UPDATE). We expect the command
// to detect that this update can't be applied because it is in a CONFLICT
// state.
TEST_F(ApplyUpdatesCommandTest, SimpleConflict) {
entry_factory_->CreateUnappliedAndUnsyncedItem("item", BOOKMARKS);
ExpectGroupToChange(apply_updates_command_, GROUP_UI);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(1, status->conflict_progress()->SimpleConflictingItemsSize())
<< "Unsynced and unapplied item should be a simple conflict";
}
// Runs the ApplyUpdatesCommand on an item that has both local and remote
// modifications *and* the remote modification cannot be applied without
// violating the tree constraints. We expect the command to detect that this
// update can't be applied and that this situation can't be resolved with the
// simple conflict processing logic; it is in a CONFLICT_HIERARCHY state.
TEST_F(ApplyUpdatesCommandTest, HierarchyAndSimpleConflict) {
// Create a simply-conflicting item. It will start with valid parent ids.
int64 handle = entry_factory_->CreateUnappliedAndUnsyncedItem(
"orphaned_by_server", BOOKMARKS);
{
// Manually set the SERVER_PARENT_ID to bad value.
// A bad parent indicates a hierarchy conflict.
WriteTransaction trans(FROM_HERE, UNITTEST, directory());
MutableEntry entry(&trans, syncable::GET_BY_HANDLE, handle);
ASSERT_TRUE(entry.good());
entry.Put(syncable::SERVER_PARENT_ID,
id_factory_.MakeServer("bogus_parent"));
}
ExpectGroupToChange(apply_updates_command_, GROUP_UI);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
EXPECT_EQ(1, status->update_progress()->AppliedUpdatesSize());
// An update that is both a simple conflict and a hierarchy conflict should be
// treated as a hierarchy conflict.
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(1, status->conflict_progress()->HierarchyConflictingItemsSize());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize());
}
// Runs the ApplyUpdatesCommand on an item with remote modifications that would
// create a directory loop if the update were applied. We expect the command to
// detect that this update can't be applied because it is in a
// CONFLICT_HIERARCHY state.
TEST_F(ApplyUpdatesCommandTest, HierarchyConflictDirectoryLoop) {
// Item 'X' locally has parent of 'root'. Server is updating it to have
// parent of 'Y'.
{
// Create it as a child of root node.
int64 handle = entry_factory_->CreateSyncedItem("X", BOOKMARKS, true);
WriteTransaction trans(FROM_HERE, UNITTEST, directory());
MutableEntry entry(&trans, syncable::GET_BY_HANDLE, handle);
ASSERT_TRUE(entry.good());
// Re-parent from root to "Y"
entry.Put(syncable::SERVER_VERSION, entry_factory_->GetNextRevision());
entry.Put(syncable::IS_UNAPPLIED_UPDATE, true);
entry.Put(syncable::SERVER_PARENT_ID, id_factory_.MakeServer("Y"));
}
// Item 'Y' is child of 'X'.
entry_factory_->CreateUnsyncedItem(
id_factory_.MakeServer("Y"), id_factory_.MakeServer("X"), "Y", true,
BOOKMARKS, NULL);
// If the server's update were applied, we would have X be a child of Y, and Y
// as a child of X. That's a directory loop. The UpdateApplicator should
// prevent the update from being applied and note that this is a hierarchy
// conflict.
ExpectGroupToChange(apply_updates_command_, GROUP_UI);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
EXPECT_EQ(1, status->update_progress()->AppliedUpdatesSize());
// This should count as a hierarchy conflict.
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(1, status->conflict_progress()->HierarchyConflictingItemsSize());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize());
}
// Runs the ApplyUpdatesCommand on a directory where the server sent us an
// update to add a child to a locally deleted (and unsynced) parent. We expect
// the command to not apply the update and to indicate the update is in a
// CONFLICT_HIERARCHY state.
TEST_F(ApplyUpdatesCommandTest, HierarchyConflictDeletedParent) {
// Create a locally deleted parent item.
int64 parent_handle;
entry_factory_->CreateUnsyncedItem(
Id::CreateFromServerId("parent"), id_factory_.root(),
"parent", true, BOOKMARKS, &parent_handle);
{
WriteTransaction trans(FROM_HERE, UNITTEST, directory());
MutableEntry entry(&trans, syncable::GET_BY_HANDLE, parent_handle);
entry.Put(syncable::IS_DEL, true);
}
// Create an incoming child from the server.
entry_factory_->CreateUnappliedNewItemWithParent(
"child", DefaultBookmarkSpecifics(), "parent");
// The server's update may seem valid to some other client, but on this client
// that new item's parent no longer exists. The update should not be applied
// and the update applicator should indicate this is a hierarchy conflict.
ExpectGroupToChange(apply_updates_command_, GROUP_UI);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
// This should count as a hierarchy conflict.
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(1, status->conflict_progress()->HierarchyConflictingItemsSize());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize());
}
// Runs the ApplyUpdatesCommand on a directory where the server is trying to
// delete a folder that has a recently added (and unsynced) child. We expect
// the command to not apply the update because it is in a CONFLICT_HIERARCHY
// state.
TEST_F(ApplyUpdatesCommandTest, HierarchyConflictDeleteNonEmptyDirectory) {
// Create a server-deleted directory.
{
// Create it as a child of root node.
int64 handle =
entry_factory_->CreateSyncedItem("parent", BOOKMARKS, true);
WriteTransaction trans(FROM_HERE, UNITTEST, directory());
MutableEntry entry(&trans, syncable::GET_BY_HANDLE, handle);
ASSERT_TRUE(entry.good());
// Delete it on the server.
entry.Put(syncable::SERVER_VERSION, entry_factory_->GetNextRevision());
entry.Put(syncable::IS_UNAPPLIED_UPDATE, true);
entry.Put(syncable::SERVER_PARENT_ID, id_factory_.root());
entry.Put(syncable::SERVER_IS_DEL, true);
}
// Create a local child of the server-deleted directory.
entry_factory_->CreateUnsyncedItem(
id_factory_.MakeServer("child"), id_factory_.MakeServer("parent"),
"child", false, BOOKMARKS, NULL);
// The server's request to delete the directory must be ignored, otherwise our
// unsynced new child would be orphaned. This is a hierarchy conflict.
ExpectGroupToChange(apply_updates_command_, GROUP_UI);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
// This should count as a hierarchy conflict.
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(1, status->conflict_progress()->HierarchyConflictingItemsSize());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize());
}
// Runs the ApplyUpdatesCommand on a server-created item that has a locally
// unknown parent. We expect the command to not apply the update because the
// item is in a CONFLICT_HIERARCHY state.
TEST_F(ApplyUpdatesCommandTest, HierarchyConflictUnknownParent) {
// We shouldn't be able to do anything with either of these items.
entry_factory_->CreateUnappliedNewItemWithParent(
"some_item", DefaultBookmarkSpecifics(), "unknown_parent");
entry_factory_->CreateUnappliedNewItemWithParent(
"some_other_item", DefaultBookmarkSpecifics(), "some_item");
ExpectGroupToChange(apply_updates_command_, GROUP_UI);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
ASSERT_TRUE(status->update_progress());
EXPECT_EQ(2, status->update_progress()->AppliedUpdatesSize())
<< "All updates should have been attempted";
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize())
<< "Updates with unknown parent should not be treated as 'simple'"
<< " conflicts";
EXPECT_EQ(2, status->conflict_progress()->HierarchyConflictingItemsSize())
<< "All updates with an unknown ancestors should be in conflict";
EXPECT_EQ(0, status->update_progress()->SuccessfullyAppliedUpdateCount())
<< "No item with an unknown ancestor should be applied";
}
TEST_F(ApplyUpdatesCommandTest, ItemsBothKnownAndUnknown) {
// See what happens when there's a mixture of good and bad updates.
string root_server_id = syncable::GetNullId().GetServerId();
entry_factory_->CreateUnappliedNewItemWithParent(
"first_unknown_item", DefaultBookmarkSpecifics(), "unknown_parent");
entry_factory_->CreateUnappliedNewItemWithParent(
"first_known_item", DefaultBookmarkSpecifics(), root_server_id);
entry_factory_->CreateUnappliedNewItemWithParent(
"second_unknown_item", DefaultBookmarkSpecifics(), "unknown_parent");
entry_factory_->CreateUnappliedNewItemWithParent(
"second_known_item", DefaultBookmarkSpecifics(), "first_known_item");
entry_factory_->CreateUnappliedNewItemWithParent(
"third_known_item", DefaultBookmarkSpecifics(), "fourth_known_item");
entry_factory_->CreateUnappliedNewItemWithParent(
"fourth_known_item", DefaultBookmarkSpecifics(), root_server_id);
ExpectGroupToChange(apply_updates_command_, GROUP_UI);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
ASSERT_TRUE(status->update_progress());
EXPECT_EQ(6, status->update_progress()->AppliedUpdatesSize())
<< "All updates should have been attempted";
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(2, status->conflict_progress()->HierarchyConflictingItemsSize())
<< "The updates with unknown ancestors should be in conflict";
EXPECT_EQ(4, status->update_progress()->SuccessfullyAppliedUpdateCount())
<< "The updates with known ancestors should be successfully applied";
}
TEST_F(ApplyUpdatesCommandTest, DecryptablePassword) {
// Decryptable password updates should be applied.
Cryptographer* cryptographer;
{
// Storing the cryptographer separately is bad, but for this test we
// know it's safe.
syncable::ReadTransaction trans(FROM_HERE, directory());
cryptographer = directory()->GetCryptographer(&trans);
}
KeyParams params = {"localhost", "dummy", "foobar"};
cryptographer->AddKey(params);
sync_pb::EntitySpecifics specifics;
sync_pb::PasswordSpecificsData data;
data.set_origin("http://example.com");
cryptographer->Encrypt(data,
specifics.mutable_password()->mutable_encrypted());
entry_factory_->CreateUnappliedNewItem("item", specifics, false);
ExpectGroupToChange(apply_updates_command_, GROUP_PASSWORD);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSWORD);
ASSERT_TRUE(status->update_progress());
EXPECT_EQ(1, status->update_progress()->AppliedUpdatesSize())
<< "All updates should have been attempted";
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize())
<< "No update should be in conflict because they're all decryptable";
EXPECT_EQ(1, status->update_progress()->SuccessfullyAppliedUpdateCount())
<< "The updates that can be decrypted should be applied";
}
TEST_F(ApplyUpdatesCommandTest, UndecryptableData) {
// Undecryptable updates should not be applied.
sync_pb::EntitySpecifics encrypted_bookmark;
encrypted_bookmark.mutable_encrypted();
AddDefaultFieldValue(BOOKMARKS, &encrypted_bookmark);
string root_server_id = syncable::GetNullId().GetServerId();
entry_factory_->CreateUnappliedNewItemWithParent(
"folder", encrypted_bookmark, root_server_id);
entry_factory_->CreateUnappliedNewItem("item2", encrypted_bookmark, false);
sync_pb::EntitySpecifics encrypted_password;
encrypted_password.mutable_password();
entry_factory_->CreateUnappliedNewItem("item3", encrypted_password, false);
ExpectGroupsToChange(apply_updates_command_, GROUP_UI, GROUP_PASSWORD);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
EXPECT_TRUE(status->HasConflictingUpdates())
<< "Updates that can't be decrypted should trigger the syncer to have "
<< "conflicting updates.";
{
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
ASSERT_TRUE(status->update_progress());
EXPECT_EQ(2, status->update_progress()->AppliedUpdatesSize())
<< "All updates should have been attempted";
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize())
<< "The updates that can't be decrypted should not be in regular "
<< "conflict";
EXPECT_EQ(2, status->conflict_progress()->EncryptionConflictingItemsSize())
<< "The updates that can't be decrypted should be in encryption "
<< "conflict";
EXPECT_EQ(0, status->update_progress()->SuccessfullyAppliedUpdateCount())
<< "No update that can't be decrypted should be applied";
}
{
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSWORD);
ASSERT_TRUE(status->update_progress());
EXPECT_EQ(1, status->update_progress()->AppliedUpdatesSize())
<< "All updates should have been attempted";
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize())
<< "The updates that can't be decrypted should not be in regular "
<< "conflict";
EXPECT_EQ(1, status->conflict_progress()->EncryptionConflictingItemsSize())
<< "The updates that can't be decrypted should be in encryption "
<< "conflict";
EXPECT_EQ(0, status->update_progress()->SuccessfullyAppliedUpdateCount())
<< "No update that can't be decrypted should be applied";
}
}
TEST_F(ApplyUpdatesCommandTest, SomeUndecryptablePassword) {
// Only decryptable password updates should be applied.
{
sync_pb::EntitySpecifics specifics;
sync_pb::PasswordSpecificsData data;
data.set_origin("http://example.com/1");
{
syncable::ReadTransaction trans(FROM_HERE, directory());
Cryptographer* cryptographer = directory()->GetCryptographer(&trans);
KeyParams params = {"localhost", "dummy", "foobar"};
cryptographer->AddKey(params);
cryptographer->Encrypt(data,
specifics.mutable_password()->mutable_encrypted());
}
entry_factory_->CreateUnappliedNewItem("item1", specifics, false);
}
{
// Create a new cryptographer, independent of the one in the session.
Cryptographer cryptographer(&encryptor_);
KeyParams params = {"localhost", "dummy", "bazqux"};
cryptographer.AddKey(params);
sync_pb::EntitySpecifics specifics;
sync_pb::PasswordSpecificsData data;
data.set_origin("http://example.com/2");
cryptographer.Encrypt(data,
specifics.mutable_password()->mutable_encrypted());
entry_factory_->CreateUnappliedNewItem("item2", specifics, false);
}
ExpectGroupToChange(apply_updates_command_, GROUP_PASSWORD);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
EXPECT_TRUE(status->HasConflictingUpdates())
<< "Updates that can't be decrypted should trigger the syncer to have "
<< "conflicting updates.";
{
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSWORD);
ASSERT_TRUE(status->update_progress());
EXPECT_EQ(2, status->update_progress()->AppliedUpdatesSize())
<< "All updates should have been attempted";
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize())
<< "The updates that can't be decrypted should not be in regular "
<< "conflict";
EXPECT_EQ(1, status->conflict_progress()->EncryptionConflictingItemsSize())
<< "The updates that can't be decrypted should be in encryption "
<< "conflict";
EXPECT_EQ(1, status->update_progress()->SuccessfullyAppliedUpdateCount())
<< "The undecryptable password update shouldn't be applied";
}
}
TEST_F(ApplyUpdatesCommandTest, NigoriUpdate) {
// Storing the cryptographer separately is bad, but for this test we
// know it's safe.
Cryptographer* cryptographer;
ModelTypeSet encrypted_types;
encrypted_types.Put(PASSWORDS);
encrypted_types.Put(NIGORI);
{
syncable::ReadTransaction trans(FROM_HERE, directory());
cryptographer = directory()->GetCryptographer(&trans);
EXPECT_TRUE(cryptographer->GetEncryptedTypes().Equals(encrypted_types));
}
// Nigori node updates should update the Cryptographer.
Cryptographer other_cryptographer(&encryptor_);
KeyParams params = {"localhost", "dummy", "foobar"};
other_cryptographer.AddKey(params);
sync_pb::EntitySpecifics specifics;
sync_pb::NigoriSpecifics* nigori = specifics.mutable_nigori();
other_cryptographer.GetKeys(nigori->mutable_encrypted());
nigori->set_encrypt_bookmarks(true);
encrypted_types.Put(BOOKMARKS);
entry_factory_->CreateUnappliedNewItem(
ModelTypeToRootTag(NIGORI), specifics, true);
EXPECT_FALSE(cryptographer->has_pending_keys());
ExpectGroupToChange(apply_updates_command_, GROUP_PASSIVE);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
ASSERT_TRUE(status->update_progress());
EXPECT_EQ(1, status->update_progress()->AppliedUpdatesSize())
<< "All updates should have been attempted";
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize())
<< "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());
EXPECT_TRUE(
cryptographer->GetEncryptedTypes().Equals(ModelTypeSet::All()));
}
TEST_F(ApplyUpdatesCommandTest, NigoriUpdateForDisabledTypes) {
// Storing the cryptographer separately is bad, but for this test we
// know it's safe.
Cryptographer* cryptographer;
ModelTypeSet encrypted_types;
encrypted_types.Put(PASSWORDS);
encrypted_types.Put(NIGORI);
{
syncable::ReadTransaction trans(FROM_HERE, directory());
cryptographer = directory()->GetCryptographer(&trans);
EXPECT_TRUE(cryptographer->GetEncryptedTypes().Equals(encrypted_types));
}
// Nigori node updates should update the Cryptographer.
Cryptographer other_cryptographer(&encryptor_);
KeyParams params = {"localhost", "dummy", "foobar"};
other_cryptographer.AddKey(params);
sync_pb::EntitySpecifics specifics;
sync_pb::NigoriSpecifics* nigori = specifics.mutable_nigori();
other_cryptographer.GetKeys(nigori->mutable_encrypted());
nigori->set_encrypt_sessions(true);
nigori->set_encrypt_themes(true);
encrypted_types.Put(SESSIONS);
encrypted_types.Put(THEMES);
entry_factory_->CreateUnappliedNewItem(
ModelTypeToRootTag(NIGORI), specifics, true);
EXPECT_FALSE(cryptographer->has_pending_keys());
ExpectGroupToChange(apply_updates_command_, GROUP_PASSIVE);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
ASSERT_TRUE(status->update_progress());
EXPECT_EQ(1, status->update_progress()->AppliedUpdatesSize())
<< "All updates should have been attempted";
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize())
<< "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());
EXPECT_TRUE(
cryptographer->GetEncryptedTypes().Equals(ModelTypeSet::All()));
}
// Create some local unsynced and unencrypted data. Apply a nigori update that
// turns on encryption for the unsynced data. Ensure we properly encrypt the
// data as part of the nigori update. Apply another nigori update with no
// changes. Ensure we ignore already-encrypted unsynced data and that nothing
// breaks.
TEST_F(ApplyUpdatesCommandTest, EncryptUnsyncedChanges) {
// Storing the cryptographer separately is bad, but for this test we
// know it's safe.
Cryptographer* cryptographer;
ModelTypeSet encrypted_types;
encrypted_types.Put(PASSWORDS);
encrypted_types.Put(NIGORI);
{
syncable::ReadTransaction trans(FROM_HERE, directory());
cryptographer = directory()->GetCryptographer(&trans);
EXPECT_TRUE(cryptographer->GetEncryptedTypes().Equals(encrypted_types));
// With default encrypted_types, this should be true.
EXPECT_TRUE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types));
Syncer::UnsyncedMetaHandles handles;
GetUnsyncedEntries(&trans, &handles);
EXPECT_TRUE(handles.empty());
}
// Create unsynced bookmarks without encryption.
// First item is a folder
Id folder_id = id_factory_.NewLocalId();
entry_factory_->CreateUnsyncedItem(folder_id, id_factory_.root(), "folder",
true, 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) {
entry_factory_->CreateUnsyncedItem(id_factory_.NewLocalId(), folder_id,
base::StringPrintf("Item %"PRIuS"", i),
false, BOOKMARKS, NULL);
}
// Next five items are children of the root.
for (; i < 2*batch_s; ++i) {
entry_factory_->CreateUnsyncedItem(
id_factory_.NewLocalId(), id_factory_.root(),
base::StringPrintf("Item %"PRIuS"", i), false,
BOOKMARKS, NULL);
}
KeyParams params = {"localhost", "dummy", "foobar"};
cryptographer->AddKey(params);
sync_pb::EntitySpecifics specifics;
sync_pb::NigoriSpecifics* nigori = specifics.mutable_nigori();
cryptographer->GetKeys(nigori->mutable_encrypted());
nigori->set_encrypt_bookmarks(true);
encrypted_types.Put(BOOKMARKS);
entry_factory_->CreateUnappliedNewItem(
ModelTypeToRootTag(NIGORI), specifics, true);
EXPECT_FALSE(cryptographer->has_pending_keys());
EXPECT_TRUE(cryptographer->is_ready());
{
// Ensure we have unsynced nodes that aren't properly encrypted.
syncable::ReadTransaction trans(FROM_HERE, directory());
EXPECT_FALSE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types));
Syncer::UnsyncedMetaHandles handles;
GetUnsyncedEntries(&trans, &handles);
EXPECT_EQ(2*batch_s+1, handles.size());
}
ExpectGroupToChange(apply_updates_command_, GROUP_PASSIVE);
apply_updates_command_.ExecuteImpl(session());
{
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
ASSERT_TRUE(status->update_progress());
EXPECT_EQ(1, status->update_progress()->AppliedUpdatesSize())
<< "All updates should have been attempted";
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize())
<< "No updates should be in conflict";
EXPECT_EQ(0, status->conflict_progress()->EncryptionConflictingItemsSize())
<< "No updates should 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());
{
syncable::ReadTransaction trans(FROM_HERE, directory());
// If ProcessUnsyncedChangesForEncryption worked, all our unsynced changes
// should be encrypted now.
EXPECT_TRUE(ModelTypeSet::All().Equals(
cryptographer->GetEncryptedTypes()));
EXPECT_TRUE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types));
Syncer::UnsyncedMetaHandles handles;
GetUnsyncedEntries(&trans, &handles);
EXPECT_EQ(2*batch_s+1, handles.size());
}
// Simulate another nigori update that doesn't change anything.
{
WriteTransaction trans(FROM_HERE, UNITTEST, directory());
MutableEntry entry(&trans, syncable::GET_BY_SERVER_TAG,
ModelTypeToRootTag(NIGORI));
ASSERT_TRUE(entry.good());
entry.Put(syncable::SERVER_VERSION, entry_factory_->GetNextRevision());
entry.Put(syncable::IS_UNAPPLIED_UPDATE, true);
}
ExpectGroupToChange(apply_updates_command_, GROUP_PASSIVE);
apply_updates_command_.ExecuteImpl(session());
{
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
ASSERT_TRUE(status->update_progress());
EXPECT_EQ(2, status->update_progress()->AppliedUpdatesSize())
<< "All updates should have been attempted";
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize())
<< "No updates should be in conflict";
EXPECT_EQ(0, status->conflict_progress()->EncryptionConflictingItemsSize())
<< "No updates should be in conflict";
EXPECT_EQ(2, status->update_progress()->SuccessfullyAppliedUpdateCount())
<< "The nigori update should be applied";
}
EXPECT_FALSE(cryptographer->has_pending_keys());
EXPECT_TRUE(cryptographer->is_ready());
{
syncable::ReadTransaction trans(FROM_HERE, directory());
// All our changes should still be encrypted.
EXPECT_TRUE(ModelTypeSet::All().Equals(
cryptographer->GetEncryptedTypes()));
EXPECT_TRUE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types));
Syncer::UnsyncedMetaHandles handles;
GetUnsyncedEntries(&trans, &handles);
EXPECT_EQ(2*batch_s+1, handles.size());
}
}
TEST_F(ApplyUpdatesCommandTest, CannotEncryptUnsyncedChanges) {
// Storing the cryptographer separately is bad, but for this test we
// know it's safe.
Cryptographer* cryptographer;
ModelTypeSet encrypted_types;
encrypted_types.Put(PASSWORDS);
encrypted_types.Put(NIGORI);
{
syncable::ReadTransaction trans(FROM_HERE, directory());
cryptographer = directory()->GetCryptographer(&trans);
EXPECT_TRUE(cryptographer->GetEncryptedTypes().Equals(encrypted_types));
// With default encrypted_types, this should be true.
EXPECT_TRUE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types));
Syncer::UnsyncedMetaHandles handles;
GetUnsyncedEntries(&trans, &handles);
EXPECT_TRUE(handles.empty());
}
// Create unsynced bookmarks without encryption.
// First item is a folder
Id folder_id = id_factory_.NewLocalId();
entry_factory_->CreateUnsyncedItem(
folder_id, id_factory_.root(), "folder", true,
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) {
entry_factory_->CreateUnsyncedItem(id_factory_.NewLocalId(), folder_id,
base::StringPrintf("Item %"PRIuS"", i),
false, BOOKMARKS, NULL);
}
// Next five items are children of the root.
for (; i < 2*batch_s; ++i) {
entry_factory_->CreateUnsyncedItem(
id_factory_.NewLocalId(), id_factory_.root(),
base::StringPrintf("Item %"PRIuS"", i), false,
BOOKMARKS, NULL);
}
// We encrypt with new keys, triggering the local cryptographer to be unready
// and unable to decrypt data (once updated).
Cryptographer other_cryptographer(&encryptor_);
KeyParams params = {"localhost", "dummy", "foobar"};
other_cryptographer.AddKey(params);
sync_pb::EntitySpecifics specifics;
sync_pb::NigoriSpecifics* nigori = specifics.mutable_nigori();
other_cryptographer.GetKeys(nigori->mutable_encrypted());
nigori->set_encrypt_bookmarks(true);
encrypted_types.Put(BOOKMARKS);
entry_factory_->CreateUnappliedNewItem(
ModelTypeToRootTag(NIGORI), specifics, true);
EXPECT_FALSE(cryptographer->has_pending_keys());
{
// Ensure we have unsynced nodes that aren't properly encrypted.
syncable::ReadTransaction trans(FROM_HERE, directory());
EXPECT_FALSE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types));
Syncer::UnsyncedMetaHandles handles;
GetUnsyncedEntries(&trans, &handles);
EXPECT_EQ(2*batch_s+1, handles.size());
}
ExpectGroupToChange(apply_updates_command_, GROUP_PASSIVE);
apply_updates_command_.ExecuteImpl(session());
sessions::StatusController* status = session()->mutable_status_controller();
sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
ASSERT_TRUE(status->update_progress());
EXPECT_EQ(1, status->update_progress()->AppliedUpdatesSize())
<< "All updates should have been attempted";
ASSERT_TRUE(status->conflict_progress());
EXPECT_EQ(0, status->conflict_progress()->SimpleConflictingItemsSize())
<< "The unsynced changes don't trigger a blocking conflict with the "
<< "nigori update.";
EXPECT_EQ(0, status->conflict_progress()->EncryptionConflictingItemsSize())
<< "The unsynced changes don't trigger an encryption conflict with the "
<< "nigori update.";
EXPECT_EQ(1, status->update_progress()->SuccessfullyAppliedUpdateCount())
<< "The nigori update should be applied";
EXPECT_FALSE(cryptographer->is_ready());
EXPECT_TRUE(cryptographer->has_pending_keys());
{
syncable::ReadTransaction trans(FROM_HERE, directory());
// Since we have pending keys, we would have failed to encrypt, but the
// cryptographer should be updated.
EXPECT_FALSE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types));
EXPECT_TRUE(cryptographer->GetEncryptedTypes().Equals(
ModelTypeSet().All()));
EXPECT_FALSE(cryptographer->is_ready());
EXPECT_TRUE(cryptographer->has_pending_keys());
Syncer::UnsyncedMetaHandles handles;
GetUnsyncedEntries(&trans, &handles);
EXPECT_EQ(2*batch_s+1, handles.size());
}
}
} // namespace syncer