Home | History | Annotate | Download | only in engine
      1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include <string>
      6 
      7 #include "base/format_macros.h"
      8 #include "base/string_util.h"
      9 #include "chrome/browser/sync/engine/apply_updates_command.h"
     10 #include "chrome/browser/sync/engine/syncer.h"
     11 #include "chrome/browser/sync/engine/syncer_util.h"
     12 #include "chrome/browser/sync/protocol/bookmark_specifics.pb.h"
     13 #include "chrome/browser/sync/sessions/sync_session.h"
     14 #include "chrome/browser/sync/syncable/directory_manager.h"
     15 #include "chrome/browser/sync/syncable/nigori_util.h"
     16 #include "chrome/browser/sync/syncable/syncable.h"
     17 #include "chrome/browser/sync/syncable/syncable_id.h"
     18 #include "chrome/test/sync/engine/syncer_command_test.h"
     19 #include "chrome/test/sync/engine/test_id_factory.h"
     20 #include "testing/gtest/include/gtest/gtest.h"
     21 
     22 namespace browser_sync {
     23 
     24 using sessions::SyncSession;
     25 using std::string;
     26 using syncable::Entry;
     27 using syncable::GetEncryptedDataTypes;
     28 using syncable::Id;
     29 using syncable::MutableEntry;
     30 using syncable::ReadTransaction;
     31 using syncable::ScopedDirLookup;
     32 using syncable::UNITTEST;
     33 using syncable::WriteTransaction;
     34 
     35 // A test fixture for tests exercising ApplyUpdatesCommand.
     36 class ApplyUpdatesCommandTest : public SyncerCommandTest {
     37  public:
     38  protected:
     39   ApplyUpdatesCommandTest() : next_revision_(1) {}
     40   virtual ~ApplyUpdatesCommandTest() {}
     41 
     42   virtual void SetUp() {
     43     workers()->clear();
     44     mutable_routing_info()->clear();
     45     // GROUP_PASSIVE worker.
     46     workers()->push_back(make_scoped_refptr(new ModelSafeWorker()));
     47     (*mutable_routing_info())[syncable::BOOKMARKS] = GROUP_PASSIVE;
     48     (*mutable_routing_info())[syncable::PASSWORDS] = GROUP_PASSIVE;
     49     (*mutable_routing_info())[syncable::NIGORI] = GROUP_PASSIVE;
     50     SyncerCommandTest::SetUp();
     51   }
     52 
     53   // Create a new unapplied bookmark node with a parent.
     54   void CreateUnappliedNewItemWithParent(const string& item_id,
     55                                         const string& parent_id) {
     56     ScopedDirLookup dir(syncdb()->manager(), syncdb()->name());
     57     ASSERT_TRUE(dir.good());
     58     WriteTransaction trans(dir, UNITTEST, __FILE__, __LINE__);
     59     MutableEntry entry(&trans, syncable::CREATE_NEW_UPDATE_ITEM,
     60         Id::CreateFromServerId(item_id));
     61     ASSERT_TRUE(entry.good());
     62     entry.Put(syncable::SERVER_VERSION, next_revision_++);
     63     entry.Put(syncable::IS_UNAPPLIED_UPDATE, true);
     64 
     65     entry.Put(syncable::SERVER_NON_UNIQUE_NAME, item_id);
     66     entry.Put(syncable::SERVER_PARENT_ID, Id::CreateFromServerId(parent_id));
     67     entry.Put(syncable::SERVER_IS_DIR, true);
     68     sync_pb::EntitySpecifics default_bookmark_specifics;
     69     default_bookmark_specifics.MutableExtension(sync_pb::bookmark);
     70     entry.Put(syncable::SERVER_SPECIFICS, default_bookmark_specifics);
     71   }
     72 
     73   // Create a new unapplied update without a parent.
     74   void CreateUnappliedNewItem(const string& item_id,
     75                               const sync_pb::EntitySpecifics& specifics,
     76                               bool is_unique) {
     77     ScopedDirLookup dir(syncdb()->manager(), syncdb()->name());
     78     ASSERT_TRUE(dir.good());
     79     WriteTransaction trans(dir, UNITTEST, __FILE__, __LINE__);
     80     MutableEntry entry(&trans, syncable::CREATE_NEW_UPDATE_ITEM,
     81         Id::CreateFromServerId(item_id));
     82     ASSERT_TRUE(entry.good());
     83     entry.Put(syncable::SERVER_VERSION, next_revision_++);
     84     entry.Put(syncable::IS_UNAPPLIED_UPDATE, true);
     85     entry.Put(syncable::SERVER_NON_UNIQUE_NAME, item_id);
     86     entry.Put(syncable::SERVER_PARENT_ID, syncable::kNullId);
     87     entry.Put(syncable::SERVER_IS_DIR, false);
     88     entry.Put(syncable::SERVER_SPECIFICS, specifics);
     89     if (is_unique)  // For top-level nodes.
     90       entry.Put(syncable::UNIQUE_SERVER_TAG, item_id);
     91   }
     92 
     93   // Create an unsynced item in the database.  If item_id is a local ID, it
     94   // will be treated as a create-new.  Otherwise, if it's a server ID, we'll
     95   // fake the server data so that it looks like it exists on the server.
     96   // Returns the methandle of the created item in |metahandle_out| if not NULL.
     97   void CreateUnsyncedItem(const Id& item_id,
     98                           const Id& parent_id,
     99                           const string& name,
    100                           bool is_folder,
    101                           syncable::ModelType model_type,
    102                           int64* metahandle_out) {
    103     ScopedDirLookup dir(syncdb()->manager(), syncdb()->name());
    104     ASSERT_TRUE(dir.good());
    105     WriteTransaction trans(dir, UNITTEST, __FILE__, __LINE__);
    106     Id predecessor_id = dir->GetLastChildId(&trans, parent_id);
    107     MutableEntry entry(&trans, syncable::CREATE, parent_id, name);
    108     ASSERT_TRUE(entry.good());
    109     entry.Put(syncable::ID, item_id);
    110     entry.Put(syncable::BASE_VERSION,
    111         item_id.ServerKnows() ? next_revision_++ : 0);
    112     entry.Put(syncable::IS_UNSYNCED, true);
    113     entry.Put(syncable::IS_DIR, is_folder);
    114     entry.Put(syncable::IS_DEL, false);
    115     entry.Put(syncable::PARENT_ID, parent_id);
    116     entry.PutPredecessor(predecessor_id);
    117     sync_pb::EntitySpecifics default_specifics;
    118     syncable::AddDefaultExtensionValue(model_type, &default_specifics);
    119     entry.Put(syncable::SPECIFICS, default_specifics);
    120     if (item_id.ServerKnows()) {
    121       entry.Put(syncable::SERVER_SPECIFICS, default_specifics);
    122       entry.Put(syncable::SERVER_IS_DIR, is_folder);
    123       entry.Put(syncable::SERVER_PARENT_ID, parent_id);
    124       entry.Put(syncable::SERVER_IS_DEL, false);
    125     }
    126     if (metahandle_out)
    127       *metahandle_out = entry.Get(syncable::META_HANDLE);
    128   }
    129 
    130   ApplyUpdatesCommand apply_updates_command_;
    131   TestIdFactory id_factory_;
    132  private:
    133   int64 next_revision_;
    134   DISALLOW_COPY_AND_ASSIGN(ApplyUpdatesCommandTest);
    135 };
    136 
    137 TEST_F(ApplyUpdatesCommandTest, Simple) {
    138   string root_server_id = syncable::kNullId.GetServerId();
    139   CreateUnappliedNewItemWithParent("parent", root_server_id);
    140   CreateUnappliedNewItemWithParent("child", "parent");
    141 
    142   apply_updates_command_.ExecuteImpl(session());
    143 
    144   sessions::StatusController* status = session()->status_controller();
    145 
    146   sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
    147   EXPECT_EQ(2, status->update_progress().AppliedUpdatesSize())
    148       << "All updates should have been attempted";
    149   EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize())
    150       << "Simple update shouldn't result in conflicts";
    151   EXPECT_EQ(2, status->update_progress().SuccessfullyAppliedUpdateCount())
    152       << "All items should have been successfully applied";
    153 }
    154 
    155 TEST_F(ApplyUpdatesCommandTest, UpdateWithChildrenBeforeParents) {
    156   // Set a bunch of updates which are difficult to apply in the order
    157   // they're received due to dependencies on other unseen items.
    158   string root_server_id = syncable::kNullId.GetServerId();
    159   CreateUnappliedNewItemWithParent("a_child_created_first", "parent");
    160   CreateUnappliedNewItemWithParent("x_child_created_first", "parent");
    161   CreateUnappliedNewItemWithParent("parent", root_server_id);
    162   CreateUnappliedNewItemWithParent("a_child_created_second", "parent");
    163   CreateUnappliedNewItemWithParent("x_child_created_second", "parent");
    164 
    165   apply_updates_command_.ExecuteImpl(session());
    166 
    167   sessions::StatusController* status = session()->status_controller();
    168   sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
    169   EXPECT_EQ(5, status->update_progress().AppliedUpdatesSize())
    170       << "All updates should have been attempted";
    171   EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize())
    172       << "Simple update shouldn't result in conflicts, even if out-of-order";
    173   EXPECT_EQ(5, status->update_progress().SuccessfullyAppliedUpdateCount())
    174       << "All updates should have been successfully applied";
    175 }
    176 
    177 TEST_F(ApplyUpdatesCommandTest, NestedItemsWithUnknownParent) {
    178   // We shouldn't be able to do anything with either of these items.
    179   CreateUnappliedNewItemWithParent("some_item", "unknown_parent");
    180   CreateUnappliedNewItemWithParent("some_other_item", "some_item");
    181 
    182   apply_updates_command_.ExecuteImpl(session());
    183 
    184   sessions::StatusController* status = session()->status_controller();
    185   sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
    186   EXPECT_EQ(2, status->update_progress().AppliedUpdatesSize())
    187       << "All updates should have been attempted";
    188   EXPECT_EQ(2, status->conflict_progress().ConflictingItemsSize())
    189       << "All updates with an unknown ancestors should be in conflict";
    190   EXPECT_EQ(0, status->update_progress().SuccessfullyAppliedUpdateCount())
    191       << "No item with an unknown ancestor should be applied";
    192 }
    193 
    194 TEST_F(ApplyUpdatesCommandTest, ItemsBothKnownAndUnknown) {
    195   // See what happens when there's a mixture of good and bad updates.
    196   string root_server_id = syncable::kNullId.GetServerId();
    197   CreateUnappliedNewItemWithParent("first_unknown_item", "unknown_parent");
    198   CreateUnappliedNewItemWithParent("first_known_item", root_server_id);
    199   CreateUnappliedNewItemWithParent("second_unknown_item", "unknown_parent");
    200   CreateUnappliedNewItemWithParent("second_known_item", "first_known_item");
    201   CreateUnappliedNewItemWithParent("third_known_item", "fourth_known_item");
    202   CreateUnappliedNewItemWithParent("fourth_known_item", root_server_id);
    203 
    204   apply_updates_command_.ExecuteImpl(session());
    205 
    206   sessions::StatusController* status = session()->status_controller();
    207   sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
    208   EXPECT_EQ(6, status->update_progress().AppliedUpdatesSize())
    209       << "All updates should have been attempted";
    210   EXPECT_EQ(2, status->conflict_progress().ConflictingItemsSize())
    211       << "The updates with unknown ancestors should be in conflict";
    212   EXPECT_EQ(4, status->update_progress().SuccessfullyAppliedUpdateCount())
    213       << "The updates with known ancestors should be successfully applied";
    214 }
    215 
    216 TEST_F(ApplyUpdatesCommandTest, DecryptablePassword) {
    217   // Decryptable password updates should be applied.
    218   Cryptographer* cryptographer;
    219   {
    220       // Storing the cryptographer separately is bad, but for this test we
    221       // know it's safe.
    222       ScopedDirLookup dir(syncdb()->manager(), syncdb()->name());
    223       ASSERT_TRUE(dir.good());
    224       ReadTransaction trans(dir, __FILE__, __LINE__);
    225       cryptographer =
    226           session()->context()->directory_manager()->GetCryptographer(&trans);
    227   }
    228 
    229   browser_sync::KeyParams params = {"localhost", "dummy", "foobar"};
    230   cryptographer->AddKey(params);
    231 
    232   sync_pb::EntitySpecifics specifics;
    233   sync_pb::PasswordSpecificsData data;
    234   data.set_origin("http://example.com");
    235 
    236   cryptographer->Encrypt(data,
    237       specifics.MutableExtension(sync_pb::password)->mutable_encrypted());
    238   CreateUnappliedNewItem("item", specifics, false);
    239 
    240   apply_updates_command_.ExecuteImpl(session());
    241 
    242   sessions::StatusController* status = session()->status_controller();
    243   sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
    244   EXPECT_EQ(1, status->update_progress().AppliedUpdatesSize())
    245       << "All updates should have been attempted";
    246   EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize())
    247       << "No update should be in conflict because they're all decryptable";
    248   EXPECT_EQ(1, status->update_progress().SuccessfullyAppliedUpdateCount())
    249       << "The updates that can be decrypted should be applied";
    250 }
    251 
    252 TEST_F(ApplyUpdatesCommandTest, UndecryptablePassword) {
    253   // Undecryptable password updates should not be applied.
    254   sync_pb::EntitySpecifics specifics;
    255   specifics.MutableExtension(sync_pb::password);
    256   CreateUnappliedNewItem("item", specifics, false);
    257 
    258   apply_updates_command_.ExecuteImpl(session());
    259 
    260   sessions::StatusController* status = session()->status_controller();
    261   sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
    262   EXPECT_EQ(1, status->update_progress().AppliedUpdatesSize())
    263       << "All updates should have been attempted";
    264   EXPECT_EQ(1, status->conflict_progress().ConflictingItemsSize())
    265       << "The updates that can't be decrypted should be in conflict";
    266   EXPECT_EQ(0, status->update_progress().SuccessfullyAppliedUpdateCount())
    267       << "No update that can't be decrypted should be applied";
    268 }
    269 
    270 TEST_F(ApplyUpdatesCommandTest, SomeUndecryptablePassword) {
    271   // Only decryptable password updates should be applied.
    272   {
    273     sync_pb::EntitySpecifics specifics;
    274     sync_pb::PasswordSpecificsData data;
    275     data.set_origin("http://example.com/1");
    276     {
    277       ScopedDirLookup dir(syncdb()->manager(), syncdb()->name());
    278       ASSERT_TRUE(dir.good());
    279       ReadTransaction trans(dir, __FILE__, __LINE__);
    280       Cryptographer* cryptographer =
    281           session()->context()->directory_manager()->GetCryptographer(&trans);
    282 
    283       KeyParams params = {"localhost", "dummy", "foobar"};
    284       cryptographer->AddKey(params);
    285 
    286       cryptographer->Encrypt(data,
    287           specifics.MutableExtension(sync_pb::password)->mutable_encrypted());
    288     }
    289     CreateUnappliedNewItem("item1", specifics, false);
    290   }
    291   {
    292     // Create a new cryptographer, independent of the one in the session.
    293     Cryptographer cryptographer;
    294     KeyParams params = {"localhost", "dummy", "bazqux"};
    295     cryptographer.AddKey(params);
    296 
    297     sync_pb::EntitySpecifics specifics;
    298     sync_pb::PasswordSpecificsData data;
    299     data.set_origin("http://example.com/2");
    300 
    301     cryptographer.Encrypt(data,
    302         specifics.MutableExtension(sync_pb::password)->mutable_encrypted());
    303     CreateUnappliedNewItem("item2", specifics, false);
    304   }
    305 
    306   apply_updates_command_.ExecuteImpl(session());
    307 
    308   sessions::StatusController* status = session()->status_controller();
    309   sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
    310   EXPECT_EQ(2, status->update_progress().AppliedUpdatesSize())
    311       << "All updates should have been attempted";
    312   EXPECT_EQ(1, status->conflict_progress().ConflictingItemsSize())
    313       << "The decryptable password update should be applied";
    314   EXPECT_EQ(1, status->update_progress().SuccessfullyAppliedUpdateCount())
    315       << "The undecryptable password update shouldn't be applied";
    316 }
    317 
    318 TEST_F(ApplyUpdatesCommandTest, NigoriUpdate) {
    319   // Storing the cryptographer separately is bad, but for this test we
    320   // know it's safe.
    321   Cryptographer* cryptographer;
    322   syncable::ModelTypeSet encrypted_types;
    323   {
    324     ScopedDirLookup dir(syncdb()->manager(), syncdb()->name());
    325     ASSERT_TRUE(dir.good());
    326     ReadTransaction trans(dir, __FILE__, __LINE__);
    327     EXPECT_EQ(encrypted_types, GetEncryptedDataTypes(&trans));
    328     cryptographer =
    329         session()->context()->directory_manager()->GetCryptographer(&trans);
    330   }
    331 
    332   // Nigori node updates should update the Cryptographer.
    333   Cryptographer other_cryptographer;
    334   KeyParams params = {"localhost", "dummy", "foobar"};
    335   other_cryptographer.AddKey(params);
    336 
    337   sync_pb::EntitySpecifics specifics;
    338   sync_pb::NigoriSpecifics* nigori =
    339       specifics.MutableExtension(sync_pb::nigori);
    340   other_cryptographer.GetKeys(nigori->mutable_encrypted());
    341   nigori->set_encrypt_bookmarks(true);
    342   encrypted_types.insert(syncable::BOOKMARKS);
    343   CreateUnappliedNewItem(syncable::ModelTypeToRootTag(syncable::NIGORI),
    344                          specifics, true);
    345   EXPECT_FALSE(cryptographer->has_pending_keys());
    346 
    347   apply_updates_command_.ExecuteImpl(session());
    348 
    349   sessions::StatusController* status = session()->status_controller();
    350   sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
    351   EXPECT_EQ(1, status->update_progress().AppliedUpdatesSize())
    352       << "All updates should have been attempted";
    353   EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize())
    354       << "The nigori update shouldn't be in conflict";
    355   EXPECT_EQ(1, status->update_progress().SuccessfullyAppliedUpdateCount())
    356       << "The nigori update should be applied";
    357 
    358   EXPECT_FALSE(cryptographer->is_ready());
    359   EXPECT_TRUE(cryptographer->has_pending_keys());
    360 }
    361 
    362 TEST_F(ApplyUpdatesCommandTest, EncryptUnsyncedChanges) {
    363   // Storing the cryptographer separately is bad, but for this test we
    364   // know it's safe.
    365   Cryptographer* cryptographer;
    366   syncable::ModelTypeSet encrypted_types;
    367   {
    368     ScopedDirLookup dir(syncdb()->manager(), syncdb()->name());
    369     ASSERT_TRUE(dir.good());
    370     ReadTransaction trans(dir, __FILE__, __LINE__);
    371     EXPECT_EQ(encrypted_types, GetEncryptedDataTypes(&trans));
    372     cryptographer =
    373         session()->context()->directory_manager()->GetCryptographer(&trans);
    374 
    375     // With empty encrypted_types, this should be true.
    376     EXPECT_TRUE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types));
    377 
    378     Syncer::UnsyncedMetaHandles handles;
    379     SyncerUtil::GetUnsyncedEntries(&trans, &handles);
    380     EXPECT_TRUE(handles.empty());
    381   }
    382 
    383   // Create unsynced bookmarks without encryption.
    384   // First item is a folder
    385   Id folder_id = id_factory_.NewLocalId();
    386   CreateUnsyncedItem(folder_id, id_factory_.root(), "folder",
    387                      true, syncable::BOOKMARKS, NULL);
    388   // Next five items are children of the folder
    389   size_t i;
    390   size_t batch_s = 5;
    391   for (i = 0; i < batch_s; ++i) {
    392     CreateUnsyncedItem(id_factory_.NewLocalId(), folder_id,
    393                        StringPrintf("Item %"PRIuS"", i), false,
    394                        syncable::BOOKMARKS, NULL);
    395   }
    396   // Next five items are children of the root.
    397   for (; i < 2*batch_s; ++i) {
    398     CreateUnsyncedItem(id_factory_.NewLocalId(), id_factory_.root(),
    399                        StringPrintf("Item %"PRIuS"", i), false,
    400                        syncable::BOOKMARKS, NULL);
    401   }
    402 
    403   KeyParams params = {"localhost", "dummy", "foobar"};
    404   cryptographer->AddKey(params);
    405   sync_pb::EntitySpecifics specifics;
    406   sync_pb::NigoriSpecifics* nigori =
    407       specifics.MutableExtension(sync_pb::nigori);
    408   cryptographer->GetKeys(nigori->mutable_encrypted());
    409   nigori->set_encrypt_bookmarks(true);
    410   encrypted_types.insert(syncable::BOOKMARKS);
    411   CreateUnappliedNewItem(syncable::ModelTypeToRootTag(syncable::NIGORI),
    412                          specifics, true);
    413   EXPECT_FALSE(cryptographer->has_pending_keys());
    414   EXPECT_TRUE(cryptographer->is_ready());
    415 
    416   {
    417     // Ensure we have unsynced nodes that aren't properly encrypted.
    418     ScopedDirLookup dir(syncdb()->manager(), syncdb()->name());
    419     ASSERT_TRUE(dir.good());
    420     ReadTransaction trans(dir, __FILE__, __LINE__);
    421     EXPECT_FALSE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types));
    422 
    423     Syncer::UnsyncedMetaHandles handles;
    424     SyncerUtil::GetUnsyncedEntries(&trans, &handles);
    425     EXPECT_EQ(2*batch_s+1, handles.size());
    426   }
    427 
    428   apply_updates_command_.ExecuteImpl(session());
    429 
    430   sessions::StatusController* status = session()->status_controller();
    431   sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
    432   EXPECT_EQ(1, status->update_progress().AppliedUpdatesSize())
    433       << "All updates should have been attempted";
    434   EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize())
    435       << "The nigori update shouldn't be in conflict";
    436   EXPECT_EQ(1, status->update_progress().SuccessfullyAppliedUpdateCount())
    437       << "The nigori update should be applied";
    438   EXPECT_FALSE(cryptographer->has_pending_keys());
    439   EXPECT_TRUE(cryptographer->is_ready());
    440   {
    441     ScopedDirLookup dir(syncdb()->manager(), syncdb()->name());
    442     ASSERT_TRUE(dir.good());
    443     ReadTransaction trans(dir, __FILE__, __LINE__);
    444 
    445     // If ProcessUnsyncedChangesForEncryption worked, all our unsynced changes
    446     // should be encrypted now.
    447     EXPECT_EQ(encrypted_types, GetEncryptedDataTypes(&trans));
    448     EXPECT_TRUE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types));
    449 
    450     Syncer::UnsyncedMetaHandles handles;
    451     SyncerUtil::GetUnsyncedEntries(&trans, &handles);
    452     EXPECT_EQ(2*batch_s+1, handles.size());
    453   }
    454 }
    455 
    456 TEST_F(ApplyUpdatesCommandTest, CannotEncryptUnsyncedChanges) {
    457   // Storing the cryptographer separately is bad, but for this test we
    458   // know it's safe.
    459   Cryptographer* cryptographer;
    460   syncable::ModelTypeSet encrypted_types;
    461   {
    462     ScopedDirLookup dir(syncdb()->manager(), syncdb()->name());
    463     ASSERT_TRUE(dir.good());
    464     ReadTransaction trans(dir, __FILE__, __LINE__);
    465     EXPECT_EQ(encrypted_types, GetEncryptedDataTypes(&trans));
    466     cryptographer =
    467         session()->context()->directory_manager()->GetCryptographer(&trans);
    468 
    469     // With empty encrypted_types, this should be true.
    470     EXPECT_TRUE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types));
    471 
    472     Syncer::UnsyncedMetaHandles handles;
    473     SyncerUtil::GetUnsyncedEntries(&trans, &handles);
    474     EXPECT_TRUE(handles.empty());
    475   }
    476 
    477   // Create unsynced bookmarks without encryption.
    478   // First item is a folder
    479   Id folder_id = id_factory_.NewLocalId();
    480   CreateUnsyncedItem(folder_id, id_factory_.root(), "folder", true,
    481                      syncable::BOOKMARKS, NULL);
    482   // Next five items are children of the folder
    483   size_t i;
    484   size_t batch_s = 5;
    485   for (i = 0; i < batch_s; ++i) {
    486     CreateUnsyncedItem(id_factory_.NewLocalId(), folder_id,
    487                        StringPrintf("Item %"PRIuS"", i), false,
    488                        syncable::BOOKMARKS, NULL);
    489   }
    490   // Next five items are children of the root.
    491   for (; i < 2*batch_s; ++i) {
    492     CreateUnsyncedItem(id_factory_.NewLocalId(), id_factory_.root(),
    493                        StringPrintf("Item %"PRIuS"", i), false,
    494                        syncable::BOOKMARKS, NULL);
    495   }
    496 
    497   // We encrypt with new keys, triggering the local cryptographer to be unready
    498   // and unable to decrypt data (once updated).
    499   Cryptographer other_cryptographer;
    500   KeyParams params = {"localhost", "dummy", "foobar"};
    501   other_cryptographer.AddKey(params);
    502   sync_pb::EntitySpecifics specifics;
    503   sync_pb::NigoriSpecifics* nigori =
    504       specifics.MutableExtension(sync_pb::nigori);
    505   other_cryptographer.GetKeys(nigori->mutable_encrypted());
    506   nigori->set_encrypt_bookmarks(true);
    507   encrypted_types.insert(syncable::BOOKMARKS);
    508   CreateUnappliedNewItem(syncable::ModelTypeToRootTag(syncable::NIGORI),
    509                          specifics, true);
    510   EXPECT_FALSE(cryptographer->has_pending_keys());
    511 
    512   {
    513     // Ensure we have unsynced nodes that aren't properly encrypted.
    514     ScopedDirLookup dir(syncdb()->manager(), syncdb()->name());
    515     ASSERT_TRUE(dir.good());
    516     ReadTransaction trans(dir, __FILE__, __LINE__);
    517     EXPECT_FALSE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types));
    518     Syncer::UnsyncedMetaHandles handles;
    519     SyncerUtil::GetUnsyncedEntries(&trans, &handles);
    520     EXPECT_EQ(2*batch_s+1, handles.size());
    521   }
    522 
    523   apply_updates_command_.ExecuteImpl(session());
    524 
    525   sessions::StatusController* status = session()->status_controller();
    526   sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE);
    527   EXPECT_EQ(1, status->update_progress().AppliedUpdatesSize())
    528       << "All updates should have been attempted";
    529   EXPECT_EQ(1, status->conflict_progress().ConflictingItemsSize())
    530       << "The unsynced chnages trigger a conflict with the nigori update.";
    531   EXPECT_EQ(0, status->update_progress().SuccessfullyAppliedUpdateCount())
    532       << "The nigori update should not be applied";
    533   EXPECT_FALSE(cryptographer->is_ready());
    534   EXPECT_TRUE(cryptographer->has_pending_keys());
    535   {
    536     // Ensure the unsynced nodes are still not encrypted.
    537     ScopedDirLookup dir(syncdb()->manager(), syncdb()->name());
    538     ASSERT_TRUE(dir.good());
    539     ReadTransaction trans(dir, __FILE__, __LINE__);
    540 
    541     // Since we're in conflict, the specifics don't reflect the unapplied
    542     // changes.
    543     EXPECT_FALSE(VerifyUnsyncedChangesAreEncrypted(&trans, encrypted_types));
    544     encrypted_types.clear();
    545     EXPECT_EQ(encrypted_types, GetEncryptedDataTypes(&trans));
    546 
    547     Syncer::UnsyncedMetaHandles handles;
    548     SyncerUtil::GetUnsyncedEntries(&trans, &handles);
    549     EXPECT_EQ(2*batch_s+1, handles.size());
    550   }
    551 }
    552 
    553 }  // namespace browser_sync
    554