1 // Copyright (c) 2012 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.
7 #include "base/location.h"
8 #include "base/memory/scoped_ptr.h"
9 #include "base/stringprintf.h"
10 #include "sync/engine/apply_updates_command.h"
11 #include "sync/engine/syncer.h"
12 #include "sync/internal_api/public/test/test_entry_factory.h"
13 #include "sync/protocol/bookmark_specifics.pb.h"
14 #include "sync/protocol/password_specifics.pb.h"
15 #include "sync/syncable/mutable_entry.h"
16 #include "sync/syncable/read_transaction.h"
17 #include "sync/syncable/syncable_id.h"
18 #include "sync/syncable/syncable_util.h"
19 #include "sync/syncable/write_transaction.h"
20 #include "sync/test/engine/fake_model_worker.h"
21 #include "sync/test/engine/syncer_command_test.h"
22 #include "sync/test/engine/test_id_factory.h"
23 #include "sync/test/fake_sync_encryption_handler.h"
24 #include "sync/util/cryptographer.h"
25 #include "testing/gtest/include/gtest/gtest.h"
31 using syncable::MutableEntry
;
32 using syncable::UNITTEST
;
33 using syncable::WriteTransaction
;
36 sync_pb::EntitySpecifics
DefaultBookmarkSpecifics() {
37 sync_pb::EntitySpecifics result
;
38 AddDefaultFieldValue(BOOKMARKS
, &result
);
43 // A test fixture for tests exercising ApplyUpdatesCommand.
44 class ApplyUpdatesCommandTest
: public SyncerCommandTest
{
47 ApplyUpdatesCommandTest() {}
48 virtual ~ApplyUpdatesCommandTest() {}
50 virtual void SetUp() {
52 mutable_routing_info()->clear();
54 make_scoped_refptr(new FakeModelWorker(GROUP_UI
)));
56 make_scoped_refptr(new FakeModelWorker(GROUP_PASSWORD
)));
57 (*mutable_routing_info())[BOOKMARKS
] = GROUP_UI
;
58 (*mutable_routing_info())[PASSWORDS
] = GROUP_PASSWORD
;
59 (*mutable_routing_info())[NIGORI
] = GROUP_PASSIVE
;
60 SyncerCommandTest::SetUp();
61 entry_factory_
.reset(new TestEntryFactory(directory()));
62 ExpectNoGroupsToChange(apply_updates_command_
);
66 DISALLOW_COPY_AND_ASSIGN(ApplyUpdatesCommandTest
);
68 ApplyUpdatesCommand apply_updates_command_
;
69 scoped_ptr
<TestEntryFactory
> entry_factory_
;
72 TEST_F(ApplyUpdatesCommandTest
, Simple
) {
73 string root_server_id
= syncable::GetNullId().GetServerId();
74 entry_factory_
->CreateUnappliedNewItemWithParent("parent",
75 DefaultBookmarkSpecifics(),
77 entry_factory_
->CreateUnappliedNewItemWithParent("child",
78 DefaultBookmarkSpecifics(),
81 ExpectGroupToChange(apply_updates_command_
, GROUP_UI
);
82 apply_updates_command_
.ExecuteImpl(session());
84 sessions::StatusController
* status
= session()->mutable_status_controller();
86 EXPECT_EQ(0, status
->num_simple_conflicts())
87 << "Simple update shouldn't result in conflicts";
88 EXPECT_EQ(0, status
->num_encryption_conflicts())
89 << "Simple update shouldn't result in conflicts";
90 EXPECT_EQ(0, status
->num_hierarchy_conflicts())
91 << "Simple update shouldn't result in conflicts";
92 sessions::ScopedModelSafeGroupRestriction
r(status
, GROUP_UI
);
93 ASSERT_TRUE(status
->update_progress());
94 EXPECT_EQ(2, status
->update_progress()->AppliedUpdatesSize())
95 << "All updates should have been attempted";
96 EXPECT_EQ(2, status
->update_progress()->SuccessfullyAppliedUpdateCount())
97 << "All items should have been successfully applied";
100 TEST_F(ApplyUpdatesCommandTest
, UpdateWithChildrenBeforeParents
) {
101 // Set a bunch of updates which are difficult to apply in the order
102 // they're received due to dependencies on other unseen items.
103 string root_server_id
= syncable::GetNullId().GetServerId();
104 entry_factory_
->CreateUnappliedNewItemWithParent(
105 "a_child_created_first", DefaultBookmarkSpecifics(), "parent");
106 entry_factory_
->CreateUnappliedNewItemWithParent(
107 "x_child_created_first", DefaultBookmarkSpecifics(), "parent");
108 entry_factory_
->CreateUnappliedNewItemWithParent(
109 "parent", DefaultBookmarkSpecifics(), root_server_id
);
110 entry_factory_
->CreateUnappliedNewItemWithParent(
111 "a_child_created_second", DefaultBookmarkSpecifics(), "parent");
112 entry_factory_
->CreateUnappliedNewItemWithParent(
113 "x_child_created_second", DefaultBookmarkSpecifics(), "parent");
115 ExpectGroupToChange(apply_updates_command_
, GROUP_UI
);
116 apply_updates_command_
.ExecuteImpl(session());
118 sessions::StatusController
* status
= session()->mutable_status_controller();
119 EXPECT_EQ(0, status
->num_simple_conflicts())
120 << "Simple update shouldn't result in conflicts, even if out-of-order";
121 sessions::ScopedModelSafeGroupRestriction
r(status
, GROUP_UI
);
122 ASSERT_TRUE(status
->update_progress());
123 EXPECT_EQ(5, status
->update_progress()->AppliedUpdatesSize())
124 << "All updates should have been attempted";
125 EXPECT_EQ(5, status
->update_progress()->SuccessfullyAppliedUpdateCount())
126 << "All updates should have been successfully applied";
129 // Runs the ApplyUpdatesCommand on an item that has both local and remote
130 // modifications (IS_UNSYNCED and IS_UNAPPLIED_UPDATE). We expect the command
131 // to detect that this update can't be applied because it is in a CONFLICT
133 TEST_F(ApplyUpdatesCommandTest
, SimpleConflict
) {
134 entry_factory_
->CreateUnappliedAndUnsyncedItem("item", BOOKMARKS
);
136 ExpectGroupToChange(apply_updates_command_
, GROUP_UI
);
137 apply_updates_command_
.ExecuteImpl(session());
139 sessions::StatusController
* status
= session()->mutable_status_controller();
140 EXPECT_EQ(1, status
->num_simple_conflicts())
141 << "Unsynced and unapplied item should be a simple conflict";
144 // Runs the ApplyUpdatesCommand on an item that has both local and remote
145 // modifications *and* the remote modification cannot be applied without
146 // violating the tree constraints. We expect the command to detect that this
147 // update can't be applied and that this situation can't be resolved with the
148 // simple conflict processing logic; it is in a CONFLICT_HIERARCHY state.
149 TEST_F(ApplyUpdatesCommandTest
, HierarchyAndSimpleConflict
) {
150 // Create a simply-conflicting item. It will start with valid parent ids.
151 int64 handle
= entry_factory_
->CreateUnappliedAndUnsyncedItem(
152 "orphaned_by_server", BOOKMARKS
);
154 // Manually set the SERVER_PARENT_ID to bad value.
155 // A bad parent indicates a hierarchy conflict.
156 WriteTransaction
trans(FROM_HERE
, UNITTEST
, directory());
157 MutableEntry
entry(&trans
, syncable::GET_BY_HANDLE
, handle
);
158 ASSERT_TRUE(entry
.good());
160 entry
.Put(syncable::SERVER_PARENT_ID
,
161 TestIdFactory::MakeServer("bogus_parent"));
164 ExpectGroupToChange(apply_updates_command_
, GROUP_UI
);
165 apply_updates_command_
.ExecuteImpl(session());
167 sessions::StatusController
* status
= session()->mutable_status_controller();
169 // An update that is both a simple conflict and a hierarchy conflict should be
170 // treated as a hierarchy conflict.
171 EXPECT_EQ(1, status
->num_hierarchy_conflicts());
172 EXPECT_EQ(0, status
->num_simple_conflicts());
174 sessions::ScopedModelSafeGroupRestriction
r(status
, GROUP_UI
);
175 EXPECT_EQ(1, status
->update_progress()->AppliedUpdatesSize());
179 // Runs the ApplyUpdatesCommand on an item with remote modifications that would
180 // create a directory loop if the update were applied. We expect the command to
181 // detect that this update can't be applied because it is in a
182 // CONFLICT_HIERARCHY state.
183 TEST_F(ApplyUpdatesCommandTest
, HierarchyConflictDirectoryLoop
) {
184 // Item 'X' locally has parent of 'root'. Server is updating it to have
187 // Create it as a child of root node.
188 int64 handle
= entry_factory_
->CreateSyncedItem("X", BOOKMARKS
, true);
190 WriteTransaction
trans(FROM_HERE
, UNITTEST
, directory());
191 MutableEntry
entry(&trans
, syncable::GET_BY_HANDLE
, handle
);
192 ASSERT_TRUE(entry
.good());
194 // Re-parent from root to "Y"
195 entry
.Put(syncable::SERVER_VERSION
, entry_factory_
->GetNextRevision());
196 entry
.Put(syncable::IS_UNAPPLIED_UPDATE
, true);
197 entry
.Put(syncable::SERVER_PARENT_ID
, TestIdFactory::MakeServer("Y"));
200 // Item 'Y' is child of 'X'.
201 entry_factory_
->CreateUnsyncedItem(
202 TestIdFactory::MakeServer("Y"), TestIdFactory::MakeServer("X"), "Y", true,
205 // If the server's update were applied, we would have X be a child of Y, and Y
206 // as a child of X. That's a directory loop. The UpdateApplicator should
207 // prevent the update from being applied and note that this is a hierarchy
210 ExpectGroupToChange(apply_updates_command_
, GROUP_UI
);
211 apply_updates_command_
.ExecuteImpl(session());
213 sessions::StatusController
* status
= session()->mutable_status_controller();
215 // This should count as a hierarchy conflict.
216 EXPECT_EQ(1, status
->num_hierarchy_conflicts());
217 EXPECT_EQ(0, status
->num_simple_conflicts());
219 sessions::ScopedModelSafeGroupRestriction
r(status
, GROUP_UI
);
220 EXPECT_EQ(1, status
->update_progress()->AppliedUpdatesSize());
223 // Runs the ApplyUpdatesCommand on a directory where the server sent us an
224 // update to add a child to a locally deleted (and unsynced) parent. We expect
225 // the command to not apply the update and to indicate the update is in a
226 // CONFLICT_HIERARCHY state.
227 TEST_F(ApplyUpdatesCommandTest
, HierarchyConflictDeletedParent
) {
228 // Create a locally deleted parent item.
230 entry_factory_
->CreateUnsyncedItem(
231 Id::CreateFromServerId("parent"), TestIdFactory::root(),
232 "parent", true, BOOKMARKS
, &parent_handle
);
234 WriteTransaction
trans(FROM_HERE
, UNITTEST
, directory());
235 MutableEntry
entry(&trans
, syncable::GET_BY_HANDLE
, parent_handle
);
236 entry
.Put(syncable::IS_DEL
, true);
239 // Create an incoming child from the server.
240 entry_factory_
->CreateUnappliedNewItemWithParent(
241 "child", DefaultBookmarkSpecifics(), "parent");
243 // The server's update may seem valid to some other client, but on this client
244 // that new item's parent no longer exists. The update should not be applied
245 // and the update applicator should indicate this is a hierarchy conflict.
247 ExpectGroupToChange(apply_updates_command_
, GROUP_UI
);
248 apply_updates_command_
.ExecuteImpl(session());
250 sessions::StatusController
* status
= session()->mutable_status_controller();
251 EXPECT_EQ(1, status
->num_hierarchy_conflicts());
252 EXPECT_EQ(0, status
->num_simple_conflicts());
255 // Runs the ApplyUpdatesCommand on a directory where the server is trying to
256 // delete a folder that has a recently added (and unsynced) child. We expect
257 // the command to not apply the update because it is in a CONFLICT_HIERARCHY
259 TEST_F(ApplyUpdatesCommandTest
, HierarchyConflictDeleteNonEmptyDirectory
) {
260 // Create a server-deleted directory.
262 // Create it as a child of root node.
264 entry_factory_
->CreateSyncedItem("parent", BOOKMARKS
, true);
266 WriteTransaction
trans(FROM_HERE
, UNITTEST
, directory());
267 MutableEntry
entry(&trans
, syncable::GET_BY_HANDLE
, handle
);
268 ASSERT_TRUE(entry
.good());
270 // Delete it on the server.
271 entry
.Put(syncable::SERVER_VERSION
, entry_factory_
->GetNextRevision());
272 entry
.Put(syncable::IS_UNAPPLIED_UPDATE
, true);
273 entry
.Put(syncable::SERVER_PARENT_ID
, TestIdFactory::root());
274 entry
.Put(syncable::SERVER_IS_DEL
, true);
277 // Create a local child of the server-deleted directory.
278 entry_factory_
->CreateUnsyncedItem(
279 TestIdFactory::MakeServer("child"), TestIdFactory::MakeServer("parent"),
280 "child", false, BOOKMARKS
, NULL
);
282 // The server's request to delete the directory must be ignored, otherwise our
283 // unsynced new child would be orphaned. This is a hierarchy conflict.
285 ExpectGroupToChange(apply_updates_command_
, GROUP_UI
);
286 apply_updates_command_
.ExecuteImpl(session());
288 sessions::StatusController
* status
= session()->mutable_status_controller();
289 // This should count as a hierarchy conflict.
290 EXPECT_EQ(1, status
->num_hierarchy_conflicts());
291 EXPECT_EQ(0, status
->num_simple_conflicts());
294 // Runs the ApplyUpdatesCommand on a server-created item that has a locally
295 // unknown parent. We expect the command to not apply the update because the
296 // item is in a CONFLICT_HIERARCHY state.
297 TEST_F(ApplyUpdatesCommandTest
, HierarchyConflictUnknownParent
) {
298 // We shouldn't be able to do anything with either of these items.
299 entry_factory_
->CreateUnappliedNewItemWithParent(
300 "some_item", DefaultBookmarkSpecifics(), "unknown_parent");
301 entry_factory_
->CreateUnappliedNewItemWithParent(
302 "some_other_item", DefaultBookmarkSpecifics(), "some_item");
304 ExpectGroupToChange(apply_updates_command_
, GROUP_UI
);
305 apply_updates_command_
.ExecuteImpl(session());
307 sessions::StatusController
* status
= session()->mutable_status_controller();
309 EXPECT_EQ(0, status
->num_simple_conflicts())
310 << "Updates with unknown parent should not be treated as 'simple'"
312 EXPECT_EQ(2, status
->num_hierarchy_conflicts())
313 << "All updates with an unknown ancestors should be in conflict";
315 sessions::ScopedModelSafeGroupRestriction
r(status
, GROUP_UI
);
316 ASSERT_TRUE(status
->update_progress());
317 EXPECT_EQ(2, status
->update_progress()->AppliedUpdatesSize())
318 << "All updates should have been attempted";
319 EXPECT_EQ(0, status
->update_progress()->SuccessfullyAppliedUpdateCount())
320 << "No item with an unknown ancestor should be applied";
323 TEST_F(ApplyUpdatesCommandTest
, ItemsBothKnownAndUnknown
) {
324 // See what happens when there's a mixture of good and bad updates.
325 string root_server_id
= syncable::GetNullId().GetServerId();
326 entry_factory_
->CreateUnappliedNewItemWithParent(
327 "first_unknown_item", DefaultBookmarkSpecifics(), "unknown_parent");
328 entry_factory_
->CreateUnappliedNewItemWithParent(
329 "first_known_item", DefaultBookmarkSpecifics(), root_server_id
);
330 entry_factory_
->CreateUnappliedNewItemWithParent(
331 "second_unknown_item", DefaultBookmarkSpecifics(), "unknown_parent");
332 entry_factory_
->CreateUnappliedNewItemWithParent(
333 "second_known_item", DefaultBookmarkSpecifics(), "first_known_item");
334 entry_factory_
->CreateUnappliedNewItemWithParent(
335 "third_known_item", DefaultBookmarkSpecifics(), "fourth_known_item");
336 entry_factory_
->CreateUnappliedNewItemWithParent(
337 "fourth_known_item", DefaultBookmarkSpecifics(), root_server_id
);
339 ExpectGroupToChange(apply_updates_command_
, GROUP_UI
);
340 apply_updates_command_
.ExecuteImpl(session());
342 sessions::StatusController
* status
= session()->mutable_status_controller();
344 EXPECT_EQ(2, status
->num_hierarchy_conflicts())
345 << "The updates with unknown ancestors should be in conflict";
347 sessions::ScopedModelSafeGroupRestriction
r(status
, GROUP_UI
);
348 ASSERT_TRUE(status
->update_progress());
349 EXPECT_EQ(6, status
->update_progress()->AppliedUpdatesSize())
350 << "All updates should have been attempted";
351 EXPECT_EQ(4, status
->update_progress()->SuccessfullyAppliedUpdateCount())
352 << "The updates with known ancestors should be successfully applied";
355 TEST_F(ApplyUpdatesCommandTest
, DecryptablePassword
) {
356 // Decryptable password updates should be applied.
357 Cryptographer
* cryptographer
;
359 // Storing the cryptographer separately is bad, but for this test we
361 syncable::ReadTransaction
trans(FROM_HERE
, directory());
362 cryptographer
= directory()->GetCryptographer(&trans
);
365 KeyParams params
= {"localhost", "dummy", "foobar"};
366 cryptographer
->AddKey(params
);
368 sync_pb::EntitySpecifics specifics
;
369 sync_pb::PasswordSpecificsData data
;
370 data
.set_origin("http://example.com");
372 cryptographer
->Encrypt(data
,
373 specifics
.mutable_password()->mutable_encrypted());
374 entry_factory_
->CreateUnappliedNewItem("item", specifics
, false);
376 ExpectGroupToChange(apply_updates_command_
, GROUP_PASSWORD
);
377 apply_updates_command_
.ExecuteImpl(session());
379 sessions::StatusController
* status
= session()->mutable_status_controller();
381 EXPECT_EQ(0, status
->num_simple_conflicts())
382 << "No update should be in conflict because they're all decryptable";
384 sessions::ScopedModelSafeGroupRestriction
r(status
, GROUP_PASSWORD
);
385 ASSERT_TRUE(status
->update_progress());
386 EXPECT_EQ(1, status
->update_progress()->AppliedUpdatesSize())
387 << "All updates should have been attempted";
388 EXPECT_EQ(1, status
->update_progress()->SuccessfullyAppliedUpdateCount())
389 << "The updates that can be decrypted should be applied";
392 TEST_F(ApplyUpdatesCommandTest
, UndecryptableData
) {
393 // Undecryptable updates should not be applied.
394 sync_pb::EntitySpecifics encrypted_bookmark
;
395 encrypted_bookmark
.mutable_encrypted();
396 AddDefaultFieldValue(BOOKMARKS
, &encrypted_bookmark
);
397 string root_server_id
= syncable::GetNullId().GetServerId();
398 entry_factory_
->CreateUnappliedNewItemWithParent(
399 "folder", encrypted_bookmark
, root_server_id
);
400 entry_factory_
->CreateUnappliedNewItem("item2", encrypted_bookmark
, false);
401 sync_pb::EntitySpecifics encrypted_password
;
402 encrypted_password
.mutable_password();
403 entry_factory_
->CreateUnappliedNewItem("item3", encrypted_password
, false);
405 ExpectGroupsToChange(apply_updates_command_
, GROUP_UI
, GROUP_PASSWORD
);
406 apply_updates_command_
.ExecuteImpl(session());
408 sessions::StatusController
* status
= session()->mutable_status_controller();
409 EXPECT_TRUE(status
->HasConflictingUpdates())
410 << "Updates that can't be decrypted should trigger the syncer to have "
411 << "conflicting updates.";
412 EXPECT_EQ(0, status
->num_simple_conflicts())
413 << "Updates that can't be decrypted should not be in regular conflict";
414 EXPECT_EQ(3, status
->num_encryption_conflicts())
415 << "Updates that can't be decrypted should be in encryption conflict";
417 sessions::ScopedModelSafeGroupRestriction
r(status
, GROUP_UI
);
418 ASSERT_TRUE(status
->update_progress());
419 EXPECT_EQ(2, status
->update_progress()->AppliedUpdatesSize())
420 << "All updates should have been attempted";
421 EXPECT_EQ(0, status
->update_progress()->SuccessfullyAppliedUpdateCount())
422 << "No update that can't be decrypted should be applied";
425 sessions::ScopedModelSafeGroupRestriction
r(status
, GROUP_PASSWORD
);
426 ASSERT_TRUE(status
->update_progress());
427 EXPECT_EQ(1, status
->update_progress()->AppliedUpdatesSize())
428 << "All updates should have been attempted";
429 EXPECT_EQ(0, status
->update_progress()->SuccessfullyAppliedUpdateCount())
430 << "No update that can't be decrypted should be applied";
434 TEST_F(ApplyUpdatesCommandTest
, SomeUndecryptablePassword
) {
435 Cryptographer
* cryptographer
;
436 // Only decryptable password updates should be applied.
438 sync_pb::EntitySpecifics specifics
;
439 sync_pb::PasswordSpecificsData data
;
440 data
.set_origin("http://example.com/1");
442 syncable::ReadTransaction
trans(FROM_HERE
, directory());
443 cryptographer
= directory()->GetCryptographer(&trans
);
445 KeyParams params
= {"localhost", "dummy", "foobar"};
446 cryptographer
->AddKey(params
);
448 cryptographer
->Encrypt(data
,
449 specifics
.mutable_password()->mutable_encrypted());
451 entry_factory_
->CreateUnappliedNewItem("item1", specifics
, false);
454 // Create a new cryptographer, independent of the one in the session.
455 Cryptographer
other_cryptographer(cryptographer
->encryptor());
456 KeyParams params
= {"localhost", "dummy", "bazqux"};
457 other_cryptographer
.AddKey(params
);
459 sync_pb::EntitySpecifics specifics
;
460 sync_pb::PasswordSpecificsData data
;
461 data
.set_origin("http://example.com/2");
463 other_cryptographer
.Encrypt(data
,
464 specifics
.mutable_password()->mutable_encrypted());
465 entry_factory_
->CreateUnappliedNewItem("item2", specifics
, false);
468 ExpectGroupToChange(apply_updates_command_
, GROUP_PASSWORD
);
469 apply_updates_command_
.ExecuteImpl(session());
471 sessions::StatusController
* status
= session()->mutable_status_controller();
472 EXPECT_TRUE(status
->HasConflictingUpdates())
473 << "Updates that can't be decrypted should trigger the syncer to have "
474 << "conflicting updates.";
476 EXPECT_EQ(0, status
->num_simple_conflicts())
477 << "The updates that can't be decrypted should not be in regular "
479 EXPECT_EQ(1, status
->num_encryption_conflicts())
480 << "The updates that can't be decrypted should be in encryption "
483 sessions::ScopedModelSafeGroupRestriction
r(status
, GROUP_PASSWORD
);
484 ASSERT_TRUE(status
->update_progress());
485 EXPECT_EQ(2, status
->update_progress()->AppliedUpdatesSize())
486 << "All updates should have been attempted";
487 EXPECT_EQ(1, status
->update_progress()->SuccessfullyAppliedUpdateCount())
488 << "The undecryptable password update shouldn't be applied";
492 } // namespace syncer