OLD | NEW |
| (Empty) |
1 // Copyright 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. | |
4 | |
5 #include <string> | |
6 | |
7 #include "base/location.h" | |
8 #include "base/memory/scoped_ptr.h" | |
9 #include "base/strings/stringprintf.h" | |
10 #include "sync/engine/apply_updates_and_resolve_conflicts_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/syncable_id.h" | |
17 #include "sync/syncable/syncable_read_transaction.h" | |
18 #include "sync/syncable/syncable_util.h" | |
19 #include "sync/syncable/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" | |
26 | |
27 namespace syncer { | |
28 | |
29 using std::string; | |
30 using syncable::Id; | |
31 using syncable::MutableEntry; | |
32 using syncable::UNITTEST; | |
33 using syncable::WriteTransaction; | |
34 | |
35 namespace { | |
36 sync_pb::EntitySpecifics DefaultBookmarkSpecifics() { | |
37 sync_pb::EntitySpecifics result; | |
38 AddDefaultFieldValue(BOOKMARKS, &result); | |
39 return result; | |
40 } | |
41 } // namespace | |
42 | |
43 // A test fixture for tests exercising ApplyUpdatesAndResolveConflictsCommand. | |
44 class ApplyUpdatesAndResolveConflictsCommandTest : public SyncerCommandTest { | |
45 public: | |
46 protected: | |
47 ApplyUpdatesAndResolveConflictsCommandTest() {} | |
48 virtual ~ApplyUpdatesAndResolveConflictsCommandTest() {} | |
49 | |
50 virtual void SetUp() { | |
51 workers()->clear(); | |
52 mutable_routing_info()->clear(); | |
53 workers()->push_back( | |
54 make_scoped_refptr(new FakeModelWorker(GROUP_UI))); | |
55 workers()->push_back( | |
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_); | |
63 } | |
64 | |
65 protected: | |
66 DISALLOW_COPY_AND_ASSIGN(ApplyUpdatesAndResolveConflictsCommandTest); | |
67 | |
68 ApplyUpdatesAndResolveConflictsCommand apply_updates_command_; | |
69 scoped_ptr<TestEntryFactory> entry_factory_; | |
70 }; | |
71 | |
72 TEST_F(ApplyUpdatesAndResolveConflictsCommandTest, Simple) { | |
73 string root_server_id = syncable::GetNullId().GetServerId(); | |
74 entry_factory_->CreateUnappliedNewBookmarkItemWithParent( | |
75 "parent", DefaultBookmarkSpecifics(), root_server_id); | |
76 entry_factory_->CreateUnappliedNewBookmarkItemWithParent( | |
77 "child", DefaultBookmarkSpecifics(), "parent"); | |
78 | |
79 ExpectGroupToChange(apply_updates_command_, GROUP_UI); | |
80 apply_updates_command_.ExecuteImpl(session()); | |
81 | |
82 const sessions::StatusController& status = session()->status_controller(); | |
83 EXPECT_EQ(0, status.num_encryption_conflicts()) | |
84 << "Simple update shouldn't result in conflicts"; | |
85 EXPECT_EQ(0, status.num_hierarchy_conflicts()) | |
86 << "Simple update shouldn't result in conflicts"; | |
87 EXPECT_EQ(2, status.num_updates_applied()) | |
88 << "All items should have been successfully applied"; | |
89 } | |
90 | |
91 TEST_F(ApplyUpdatesAndResolveConflictsCommandTest, | |
92 UpdateWithChildrenBeforeParents) { | |
93 // Set a bunch of updates which are difficult to apply in the order | |
94 // they're received due to dependencies on other unseen items. | |
95 string root_server_id = syncable::GetNullId().GetServerId(); | |
96 entry_factory_->CreateUnappliedNewBookmarkItemWithParent( | |
97 "a_child_created_first", DefaultBookmarkSpecifics(), "parent"); | |
98 entry_factory_->CreateUnappliedNewBookmarkItemWithParent( | |
99 "x_child_created_first", DefaultBookmarkSpecifics(), "parent"); | |
100 entry_factory_->CreateUnappliedNewBookmarkItemWithParent( | |
101 "parent", DefaultBookmarkSpecifics(), root_server_id); | |
102 entry_factory_->CreateUnappliedNewBookmarkItemWithParent( | |
103 "a_child_created_second", DefaultBookmarkSpecifics(), "parent"); | |
104 entry_factory_->CreateUnappliedNewBookmarkItemWithParent( | |
105 "x_child_created_second", DefaultBookmarkSpecifics(), "parent"); | |
106 | |
107 ExpectGroupToChange(apply_updates_command_, GROUP_UI); | |
108 apply_updates_command_.ExecuteImpl(session()); | |
109 | |
110 const sessions::StatusController& status = session()->status_controller(); | |
111 EXPECT_EQ(5, status.num_updates_applied()) | |
112 << "All updates should have been successfully applied"; | |
113 } | |
114 | |
115 // Runs the ApplyUpdatesAndResolveConflictsCommand on an item that has both | |
116 // local and remote modifications (IS_UNSYNCED and IS_UNAPPLIED_UPDATE). We | |
117 // expect the command to detect that this update can't be applied because it is | |
118 // in a CONFLICT state. | |
119 TEST_F(ApplyUpdatesAndResolveConflictsCommandTest, SimpleConflict) { | |
120 entry_factory_->CreateUnappliedAndUnsyncedBookmarkItem("item"); | |
121 | |
122 ExpectGroupToChange(apply_updates_command_, GROUP_UI); | |
123 apply_updates_command_.ExecuteImpl(session()); | |
124 | |
125 const sessions::StatusController& status = session()->status_controller(); | |
126 EXPECT_EQ(1, status.num_server_overwrites()) | |
127 << "Unsynced and unapplied item conflict should be resolved"; | |
128 EXPECT_EQ(0, status.num_updates_applied()) | |
129 << "Update should not be applied; we should override the server."; | |
130 } | |
131 | |
132 // Runs the ApplyUpdatesAndResolveConflictsCommand on an item that has both | |
133 // local and remote modifications *and* the remote modification cannot be | |
134 // applied without violating the tree constraints. We expect the command to | |
135 // detect that this update can't be applied and that this situation can't be | |
136 // resolved with the simple conflict processing logic; it is in a | |
137 // CONFLICT_HIERARCHY state. | |
138 TEST_F(ApplyUpdatesAndResolveConflictsCommandTest, HierarchyAndSimpleConflict) { | |
139 // Create a simply-conflicting item. It will start with valid parent ids. | |
140 int64 handle = entry_factory_->CreateUnappliedAndUnsyncedBookmarkItem( | |
141 "orphaned_by_server"); | |
142 { | |
143 // Manually set the SERVER_PARENT_ID to bad value. | |
144 // A bad parent indicates a hierarchy conflict. | |
145 WriteTransaction trans(FROM_HERE, UNITTEST, directory()); | |
146 MutableEntry entry(&trans, syncable::GET_BY_HANDLE, handle); | |
147 ASSERT_TRUE(entry.good()); | |
148 | |
149 entry.PutServerParentId(TestIdFactory::MakeServer("bogus_parent")); | |
150 } | |
151 | |
152 ExpectGroupToChange(apply_updates_command_, GROUP_UI); | |
153 apply_updates_command_.ExecuteImpl(session()); | |
154 | |
155 const sessions::StatusController& status = session()->status_controller(); | |
156 EXPECT_EQ(1, status.num_hierarchy_conflicts()); | |
157 } | |
158 | |
159 | |
160 // Runs the ApplyUpdatesAndResolveConflictsCommand on an item with remote | |
161 // modifications that would create a directory loop if the update were applied. | |
162 // We expect the command to detect that this update can't be applied because it | |
163 // is in a CONFLICT_HIERARCHY state. | |
164 TEST_F(ApplyUpdatesAndResolveConflictsCommandTest, | |
165 HierarchyConflictDirectoryLoop) { | |
166 // Item 'X' locally has parent of 'root'. Server is updating it to have | |
167 // parent of 'Y'. | |
168 { | |
169 // Create it as a child of root node. | |
170 int64 handle = entry_factory_->CreateSyncedItem("X", BOOKMARKS, true); | |
171 | |
172 WriteTransaction trans(FROM_HERE, UNITTEST, directory()); | |
173 MutableEntry entry(&trans, syncable::GET_BY_HANDLE, handle); | |
174 ASSERT_TRUE(entry.good()); | |
175 | |
176 // Re-parent from root to "Y" | |
177 entry.PutServerVersion(entry_factory_->GetNextRevision()); | |
178 entry.PutIsUnappliedUpdate(true); | |
179 entry.PutServerParentId(TestIdFactory::MakeServer("Y")); | |
180 } | |
181 | |
182 // Item 'Y' is child of 'X'. | |
183 entry_factory_->CreateUnsyncedItem( | |
184 TestIdFactory::MakeServer("Y"), TestIdFactory::MakeServer("X"), "Y", true, | |
185 BOOKMARKS, NULL); | |
186 | |
187 // If the server's update were applied, we would have X be a child of Y, and Y | |
188 // as a child of X. That's a directory loop. The UpdateApplicator should | |
189 // prevent the update from being applied and note that this is a hierarchy | |
190 // conflict. | |
191 | |
192 ExpectGroupToChange(apply_updates_command_, GROUP_UI); | |
193 apply_updates_command_.ExecuteImpl(session()); | |
194 | |
195 const sessions::StatusController& status = session()->status_controller(); | |
196 | |
197 // This should count as a hierarchy conflict. | |
198 EXPECT_EQ(1, status.num_hierarchy_conflicts()); | |
199 } | |
200 | |
201 // Runs the ApplyUpdatesAndResolveConflictsCommand on a directory where the | |
202 // server sent us an update to add a child to a locally deleted (and unsynced) | |
203 // parent. We expect the command to not apply the update and to indicate the | |
204 // update is in a CONFLICT_HIERARCHY state. | |
205 TEST_F(ApplyUpdatesAndResolveConflictsCommandTest, | |
206 HierarchyConflictDeletedParent) { | |
207 // Create a locally deleted parent item. | |
208 int64 parent_handle; | |
209 entry_factory_->CreateUnsyncedItem( | |
210 Id::CreateFromServerId("parent"), TestIdFactory::root(), | |
211 "parent", true, BOOKMARKS, &parent_handle); | |
212 { | |
213 WriteTransaction trans(FROM_HERE, UNITTEST, directory()); | |
214 MutableEntry entry(&trans, syncable::GET_BY_HANDLE, parent_handle); | |
215 entry.PutIsDel(true); | |
216 } | |
217 | |
218 // Create an incoming child from the server. | |
219 entry_factory_->CreateUnappliedNewItemWithParent( | |
220 "child", DefaultBookmarkSpecifics(), "parent"); | |
221 | |
222 // The server's update may seem valid to some other client, but on this client | |
223 // that new item's parent no longer exists. The update should not be applied | |
224 // and the update applicator should indicate this is a hierarchy conflict. | |
225 | |
226 ExpectGroupToChange(apply_updates_command_, GROUP_UI); | |
227 apply_updates_command_.ExecuteImpl(session()); | |
228 | |
229 const sessions::StatusController& status = session()->status_controller(); | |
230 EXPECT_EQ(1, status.num_hierarchy_conflicts()); | |
231 } | |
232 | |
233 // Runs the ApplyUpdatesAndResolveConflictsCommand on a directory where the | |
234 // server is trying to delete a folder that has a recently added (and unsynced) | |
235 // child. We expect the command to not apply the update because it is in a | |
236 // CONFLICT_HIERARCHY state. | |
237 TEST_F(ApplyUpdatesAndResolveConflictsCommandTest, | |
238 HierarchyConflictDeleteNonEmptyDirectory) { | |
239 // Create a server-deleted directory. | |
240 { | |
241 // Create it as a child of root node. | |
242 int64 handle = entry_factory_->CreateSyncedItem("parent", BOOKMARKS, true); | |
243 | |
244 WriteTransaction trans(FROM_HERE, UNITTEST, directory()); | |
245 MutableEntry entry(&trans, syncable::GET_BY_HANDLE, handle); | |
246 ASSERT_TRUE(entry.good()); | |
247 | |
248 // Delete it on the server. | |
249 entry.PutServerVersion(entry_factory_->GetNextRevision()); | |
250 entry.PutIsUnappliedUpdate(true); | |
251 entry.PutServerParentId(TestIdFactory::root()); | |
252 entry.PutServerIsDel(true); | |
253 } | |
254 | |
255 // Create a local child of the server-deleted directory. | |
256 entry_factory_->CreateUnsyncedItem( | |
257 TestIdFactory::MakeServer("child"), TestIdFactory::MakeServer("parent"), | |
258 "child", false, BOOKMARKS, NULL); | |
259 | |
260 // The server's request to delete the directory must be ignored, otherwise our | |
261 // unsynced new child would be orphaned. This is a hierarchy conflict. | |
262 | |
263 ExpectGroupToChange(apply_updates_command_, GROUP_UI); | |
264 apply_updates_command_.ExecuteImpl(session()); | |
265 | |
266 const sessions::StatusController& status = session()->status_controller(); | |
267 // This should count as a hierarchy conflict. | |
268 EXPECT_EQ(1, status.num_hierarchy_conflicts()); | |
269 } | |
270 | |
271 // Runs the ApplyUpdatesAndResolveConflictsCommand on a server-created item that | |
272 // has a locally unknown parent. We expect the command to not apply the update | |
273 // because the item is in a CONFLICT_HIERARCHY state. | |
274 TEST_F(ApplyUpdatesAndResolveConflictsCommandTest, | |
275 HierarchyConflictUnknownParent) { | |
276 // We shouldn't be able to do anything with either of these items. | |
277 entry_factory_->CreateUnappliedNewItemWithParent( | |
278 "some_item", DefaultBookmarkSpecifics(), "unknown_parent"); | |
279 entry_factory_->CreateUnappliedNewItemWithParent( | |
280 "some_other_item", DefaultBookmarkSpecifics(), "some_item"); | |
281 | |
282 ExpectGroupToChange(apply_updates_command_, GROUP_UI); | |
283 apply_updates_command_.ExecuteImpl(session()); | |
284 | |
285 const sessions::StatusController& status = session()->status_controller(); | |
286 EXPECT_EQ(2, status.num_hierarchy_conflicts()) | |
287 << "All updates with an unknown ancestors should be in conflict"; | |
288 EXPECT_EQ(0, status.num_updates_applied()) | |
289 << "No item with an unknown ancestor should be applied"; | |
290 } | |
291 | |
292 TEST_F(ApplyUpdatesAndResolveConflictsCommandTest, ItemsBothKnownAndUnknown) { | |
293 // See what happens when there's a mixture of good and bad updates. | |
294 string root_server_id = syncable::GetNullId().GetServerId(); | |
295 entry_factory_->CreateUnappliedNewItemWithParent( | |
296 "first_unknown_item", DefaultBookmarkSpecifics(), "unknown_parent"); | |
297 entry_factory_->CreateUnappliedNewItemWithParent( | |
298 "first_known_item", DefaultBookmarkSpecifics(), root_server_id); | |
299 entry_factory_->CreateUnappliedNewItemWithParent( | |
300 "second_unknown_item", DefaultBookmarkSpecifics(), "unknown_parent"); | |
301 entry_factory_->CreateUnappliedNewItemWithParent( | |
302 "second_known_item", DefaultBookmarkSpecifics(), "first_known_item"); | |
303 entry_factory_->CreateUnappliedNewItemWithParent( | |
304 "third_known_item", DefaultBookmarkSpecifics(), "fourth_known_item"); | |
305 entry_factory_->CreateUnappliedNewItemWithParent( | |
306 "fourth_known_item", DefaultBookmarkSpecifics(), root_server_id); | |
307 | |
308 ExpectGroupToChange(apply_updates_command_, GROUP_UI); | |
309 apply_updates_command_.ExecuteImpl(session()); | |
310 | |
311 const sessions::StatusController& status = session()->status_controller(); | |
312 EXPECT_EQ(2, status.num_hierarchy_conflicts()) | |
313 << "The updates with unknown ancestors should be in conflict"; | |
314 EXPECT_EQ(4, status.num_updates_applied()) | |
315 << "The updates with known ancestors should be successfully applied"; | |
316 } | |
317 | |
318 TEST_F(ApplyUpdatesAndResolveConflictsCommandTest, DecryptablePassword) { | |
319 // Decryptable password updates should be applied. | |
320 Cryptographer* cryptographer; | |
321 { | |
322 // Storing the cryptographer separately is bad, but for this test we | |
323 // know it's safe. | |
324 syncable::ReadTransaction trans(FROM_HERE, directory()); | |
325 cryptographer = directory()->GetCryptographer(&trans); | |
326 } | |
327 | |
328 KeyParams params = {"localhost", "dummy", "foobar"}; | |
329 cryptographer->AddKey(params); | |
330 | |
331 sync_pb::EntitySpecifics specifics; | |
332 sync_pb::PasswordSpecificsData data; | |
333 data.set_origin("http://example.com"); | |
334 | |
335 cryptographer->Encrypt(data, | |
336 specifics.mutable_password()->mutable_encrypted()); | |
337 entry_factory_->CreateUnappliedNewItem("item", specifics, false); | |
338 | |
339 ExpectGroupToChange(apply_updates_command_, GROUP_PASSWORD); | |
340 apply_updates_command_.ExecuteImpl(session()); | |
341 | |
342 const sessions::StatusController& status = session()->status_controller(); | |
343 EXPECT_EQ(1, status.num_updates_applied()) | |
344 << "The updates that can be decrypted should be applied"; | |
345 } | |
346 | |
347 TEST_F(ApplyUpdatesAndResolveConflictsCommandTest, UndecryptableData) { | |
348 // Undecryptable updates should not be applied. | |
349 sync_pb::EntitySpecifics encrypted_bookmark; | |
350 encrypted_bookmark.mutable_encrypted(); | |
351 AddDefaultFieldValue(BOOKMARKS, &encrypted_bookmark); | |
352 string root_server_id = syncable::GetNullId().GetServerId(); | |
353 entry_factory_->CreateUnappliedNewItemWithParent( | |
354 "folder", encrypted_bookmark, root_server_id); | |
355 entry_factory_->CreateUnappliedNewItem("item2", encrypted_bookmark, false); | |
356 sync_pb::EntitySpecifics encrypted_password; | |
357 encrypted_password.mutable_password(); | |
358 entry_factory_->CreateUnappliedNewItem("item3", encrypted_password, false); | |
359 | |
360 ExpectGroupsToChange(apply_updates_command_, GROUP_UI, GROUP_PASSWORD); | |
361 apply_updates_command_.ExecuteImpl(session()); | |
362 | |
363 const sessions::StatusController& status = session()->status_controller(); | |
364 EXPECT_EQ(3, status.num_encryption_conflicts()) | |
365 << "Updates that can't be decrypted should be in encryption conflict"; | |
366 EXPECT_EQ(0, status.num_updates_applied()) | |
367 << "No update that can't be decrypted should be applied"; | |
368 } | |
369 | |
370 TEST_F(ApplyUpdatesAndResolveConflictsCommandTest, SomeUndecryptablePassword) { | |
371 Cryptographer* cryptographer; | |
372 // Only decryptable password updates should be applied. | |
373 { | |
374 sync_pb::EntitySpecifics specifics; | |
375 sync_pb::PasswordSpecificsData data; | |
376 data.set_origin("http://example.com/1"); | |
377 { | |
378 syncable::ReadTransaction trans(FROM_HERE, directory()); | |
379 cryptographer = directory()->GetCryptographer(&trans); | |
380 | |
381 KeyParams params = {"localhost", "dummy", "foobar"}; | |
382 cryptographer->AddKey(params); | |
383 | |
384 cryptographer->Encrypt(data, | |
385 specifics.mutable_password()->mutable_encrypted()); | |
386 } | |
387 entry_factory_->CreateUnappliedNewItem("item1", specifics, false); | |
388 } | |
389 { | |
390 // Create a new cryptographer, independent of the one in the session. | |
391 Cryptographer other_cryptographer(cryptographer->encryptor()); | |
392 KeyParams params = {"localhost", "dummy", "bazqux"}; | |
393 other_cryptographer.AddKey(params); | |
394 | |
395 sync_pb::EntitySpecifics specifics; | |
396 sync_pb::PasswordSpecificsData data; | |
397 data.set_origin("http://example.com/2"); | |
398 | |
399 other_cryptographer.Encrypt(data, | |
400 specifics.mutable_password()->mutable_encrypted()); | |
401 entry_factory_->CreateUnappliedNewItem("item2", specifics, false); | |
402 } | |
403 | |
404 ExpectGroupToChange(apply_updates_command_, GROUP_PASSWORD); | |
405 apply_updates_command_.ExecuteImpl(session()); | |
406 | |
407 const sessions::StatusController& status = session()->status_controller(); | |
408 EXPECT_EQ(1, status.num_encryption_conflicts()) | |
409 << "The updates that can't be decrypted should be in encryption " | |
410 << "conflict"; | |
411 EXPECT_EQ(1, status.num_updates_applied()) | |
412 << "The undecryptable password update shouldn't be applied"; | |
413 } | |
414 | |
415 } // namespace syncer | |
OLD | NEW |