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
22namespace browser_sync {
23
24using sessions::SyncSession;
25using std::string;
26using syncable::Entry;
27using syncable::GetEncryptedDataTypes;
28using syncable::Id;
29using syncable::MutableEntry;
30using syncable::ReadTransaction;
31using syncable::ScopedDirLookup;
32using syncable::UNITTEST;
33using syncable::WriteTransaction;
34
35// A test fixture for tests exercising ApplyUpdatesCommand.
36class 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
137TEST_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
155TEST_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
177TEST_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
194TEST_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
216TEST_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
252TEST_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
270TEST_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
318TEST_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
362TEST_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
456TEST_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