1 // Copyright 2014 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 "sync/engine/directory_commit_contribution.h"
6
7 #include "base/message_loop/message_loop.h"
8 #include "sync/internal_api/public/base/attachment_id_proto.h"
9 #include "sync/sessions/status_controller.h"
10 #include "sync/syncable/entry.h"
11 #include "sync/syncable/mutable_entry.h"
12 #include "sync/syncable/syncable_read_transaction.h"
13 #include "sync/syncable/syncable_write_transaction.h"
14 #include "sync/test/engine/test_directory_setter_upper.h"
15 #include "sync/test/engine/test_id_factory.h"
16 #include "sync/test/engine/test_syncable_utils.h"
17 #include "testing/gtest/include/gtest/gtest.h"
18
19 namespace syncer {
20
21 class DirectoryCommitContributionTest : public ::testing::Test {
22 public:
SetUp()23 virtual void SetUp() OVERRIDE {
24 dir_maker_.SetUp();
25
26 syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
27 CreateTypeRoot(&trans, dir(), PREFERENCES);
28 CreateTypeRoot(&trans, dir(), EXTENSIONS);
29 CreateTypeRoot(&trans, dir(), ARTICLES);
30 CreateTypeRoot(&trans, dir(), BOOKMARKS);
31 }
32
TearDown()33 virtual void TearDown() OVERRIDE {
34 dir_maker_.TearDown();
35 }
36
37 protected:
CreateUnsyncedItemWithAttachments(syncable::WriteTransaction * trans,ModelType type,const std::string & tag,const sync_pb::AttachmentMetadata & attachment_metadata)38 int64 CreateUnsyncedItemWithAttachments(
39 syncable::WriteTransaction* trans,
40 ModelType type,
41 const std::string& tag,
42 const sync_pb::AttachmentMetadata& attachment_metadata) {
43 syncable::Entry parent_entry(trans, syncable::GET_TYPE_ROOT, type);
44 syncable::MutableEntry entry(
45 trans,
46 syncable::CREATE,
47 type,
48 parent_entry.GetId(),
49 tag);
50 if (attachment_metadata.record_size() > 0) {
51 entry.PutAttachmentMetadata(attachment_metadata);
52 }
53 entry.PutIsUnsynced(true);
54 return entry.GetMetahandle();
55 }
56
CreateUnsyncedItem(syncable::WriteTransaction * trans,ModelType type,const std::string & tag)57 int64 CreateUnsyncedItem(syncable::WriteTransaction* trans,
58 ModelType type,
59 const std::string& tag) {
60 return CreateUnsyncedItemWithAttachments(
61 trans, type, tag, sync_pb::AttachmentMetadata());
62 }
63
CreateSyncedItem(syncable::WriteTransaction * trans,ModelType type,const std::string & tag)64 int64 CreateSyncedItem(syncable::WriteTransaction* trans,
65 ModelType type,
66 const std::string& tag) {
67 syncable::Entry parent_entry(trans, syncable::GET_TYPE_ROOT, type);
68 syncable::MutableEntry entry(
69 trans,
70 syncable::CREATE,
71 type,
72 parent_entry.GetId(),
73 tag);
74
75 entry.PutId(syncable::Id::CreateFromServerId(
76 id_factory_.NewServerId().GetServerId()));
77 entry.PutBaseVersion(10);
78 entry.PutServerVersion(10);
79 entry.PutIsUnappliedUpdate(false);
80 entry.PutIsUnsynced(false);
81 entry.PutIsDel(false);
82 entry.PutServerIsDel(false);
83
84 return entry.GetMetahandle();
85 }
86
CreateSuccessfulCommitResponse(const sync_pb::SyncEntity & entity,sync_pb::CommitResponse::EntryResponse * response)87 void CreateSuccessfulCommitResponse(
88 const sync_pb::SyncEntity& entity,
89 sync_pb::CommitResponse::EntryResponse* response) {
90 response->set_response_type(sync_pb::CommitResponse::SUCCESS);
91 response->set_non_unique_name(entity.name());
92 response->set_version(entity.version() + 1);
93 response->set_parent_id_string(entity.parent_id_string());
94
95 if (entity.id_string()[0] == '-') // Look for the - in 'c-1234' style IDs.
96 response->set_id_string(id_factory_.NewServerId().GetServerId());
97 else
98 response->set_id_string(entity.id_string());
99 }
100
dir()101 syncable::Directory* dir() {
102 return dir_maker_.directory();
103 }
104
105 TestIdFactory id_factory_;
106
107 // Used in construction of DirectoryTypeDebugInfoEmitters.
108 ObserverList<TypeDebugInfoObserver> type_observers_;
109
110 private:
111 base::MessageLoop loop_; // Neeed to initialize the directory.
112 TestDirectorySetterUpper dir_maker_;
113 };
114
115 // Verify that the DirectoryCommitContribution contains only entries of its
116 // specified type.
TEST_F(DirectoryCommitContributionTest,GatherByTypes)117 TEST_F(DirectoryCommitContributionTest, GatherByTypes) {
118 int64 pref1;
119 {
120 syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
121 pref1 = CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
122 CreateUnsyncedItem(&trans, PREFERENCES, "pref2");
123 CreateUnsyncedItem(&trans, EXTENSIONS, "extension1");
124 }
125
126 DirectoryTypeDebugInfoEmitter emitter(PREFERENCES, &type_observers_);
127 scoped_ptr<DirectoryCommitContribution> cc(
128 DirectoryCommitContribution::Build(dir(), PREFERENCES, 5, &emitter));
129 ASSERT_EQ(2U, cc->GetNumEntries());
130
131 const std::vector<int64>& metahandles = cc->metahandles_;
132 EXPECT_TRUE(std::find(metahandles.begin(), metahandles.end(), pref1) !=
133 metahandles.end());
134 EXPECT_TRUE(std::find(metahandles.begin(), metahandles.end(), pref1) !=
135 metahandles.end());
136
137 cc->CleanUp();
138 }
139
140 // Verify that the DirectoryCommitContributionTest builder function
141 // truncates if necessary.
TEST_F(DirectoryCommitContributionTest,GatherAndTruncate)142 TEST_F(DirectoryCommitContributionTest, GatherAndTruncate) {
143 int64 pref1;
144 int64 pref2;
145 {
146 syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
147 pref1 = CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
148 pref2 = CreateUnsyncedItem(&trans, PREFERENCES, "pref2");
149 CreateUnsyncedItem(&trans, EXTENSIONS, "extension1");
150 }
151
152 DirectoryTypeDebugInfoEmitter emitter(PREFERENCES, &type_observers_);
153 scoped_ptr<DirectoryCommitContribution> cc(
154 DirectoryCommitContribution::Build(dir(), PREFERENCES, 1, &emitter));
155 ASSERT_EQ(1U, cc->GetNumEntries());
156
157 int64 only_metahandle = cc->metahandles_[0];
158 EXPECT_TRUE(only_metahandle == pref1 || only_metahandle == pref2);
159
160 cc->CleanUp();
161 }
162
163 // Sanity check for building commits from DirectoryCommitContributions.
164 // This test makes two CommitContribution objects of different types and uses
165 // them to initialize a commit message. Then it checks that the contents of the
166 // commit message match those of the directory they came from.
TEST_F(DirectoryCommitContributionTest,PrepareCommit)167 TEST_F(DirectoryCommitContributionTest, PrepareCommit) {
168 {
169 syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
170 CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
171 CreateUnsyncedItem(&trans, PREFERENCES, "pref2");
172 CreateUnsyncedItem(&trans, EXTENSIONS, "extension1");
173 }
174
175 DirectoryTypeDebugInfoEmitter emitter1(PREFERENCES, &type_observers_);
176 DirectoryTypeDebugInfoEmitter emitter2(EXTENSIONS, &type_observers_);
177 scoped_ptr<DirectoryCommitContribution> pref_cc(
178 DirectoryCommitContribution::Build(dir(), PREFERENCES, 25, &emitter1));
179 scoped_ptr<DirectoryCommitContribution> ext_cc(
180 DirectoryCommitContribution::Build(dir(), EXTENSIONS, 25, &emitter2));
181
182 sync_pb::ClientToServerMessage message;
183 pref_cc->AddToCommitMessage(&message);
184 ext_cc->AddToCommitMessage(&message);
185
186 const sync_pb::CommitMessage& commit_message = message.commit();
187
188 std::set<syncable::Id> ids_for_commit;
189 ASSERT_EQ(3, commit_message.entries_size());
190 for (int i = 0; i < commit_message.entries_size(); ++i) {
191 const sync_pb::SyncEntity& entity = commit_message.entries(i);
192 // The entities in this test have client-style IDs since they've never been
193 // committed before, so we must use CreateFromClientString to re-create them
194 // from the commit message.
195 ids_for_commit.insert(syncable::Id::CreateFromClientString(
196 entity.id_string()));
197 }
198
199 ASSERT_EQ(3U, ids_for_commit.size());
200 {
201 syncable::ReadTransaction trans(FROM_HERE, dir());
202 for (std::set<syncable::Id>::iterator it = ids_for_commit.begin();
203 it != ids_for_commit.end(); ++it) {
204 SCOPED_TRACE(it->value());
205 syncable::Entry entry(&trans, syncable::GET_BY_ID, *it);
206 ASSERT_TRUE(entry.good());
207 EXPECT_TRUE(entry.GetSyncing());
208 }
209 }
210
211 pref_cc->CleanUp();
212 ext_cc->CleanUp();
213 }
214
215 // Check that deletion requests include a model type.
216 // This was not always the case, but was implemented to allow us to loosen some
217 // other restrictions in the protocol.
TEST_F(DirectoryCommitContributionTest,DeletedItemsWithSpecifics)218 TEST_F(DirectoryCommitContributionTest, DeletedItemsWithSpecifics) {
219 int64 pref1;
220 {
221 syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
222 pref1 = CreateSyncedItem(&trans, PREFERENCES, "pref1");
223 syncable::MutableEntry e1(&trans, syncable::GET_BY_HANDLE, pref1);
224 e1.PutIsDel(true);
225 e1.PutIsUnsynced(true);
226 }
227
228 DirectoryTypeDebugInfoEmitter emitter(PREFERENCES, &type_observers_);
229 scoped_ptr<DirectoryCommitContribution> pref_cc(
230 DirectoryCommitContribution::Build(dir(), PREFERENCES, 25, &emitter));
231 ASSERT_TRUE(pref_cc);
232
233 sync_pb::ClientToServerMessage message;
234 pref_cc->AddToCommitMessage(&message);
235
236 const sync_pb::CommitMessage& commit_message = message.commit();
237 ASSERT_EQ(1, commit_message.entries_size());
238 EXPECT_TRUE(
239 commit_message.entries(0).specifics().has_preference());
240
241 pref_cc->CleanUp();
242 }
243
244 // As ususal, bookmarks are special. Bookmark deletion is special.
245 // Deleted bookmarks include a valid "is folder" bit and their full specifics
246 // (especially the meta info, which is what server really wants).
TEST_F(DirectoryCommitContributionTest,DeletedBookmarksWithSpecifics)247 TEST_F(DirectoryCommitContributionTest, DeletedBookmarksWithSpecifics) {
248 int64 bm1;
249 {
250 syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
251 bm1 = CreateSyncedItem(&trans, BOOKMARKS, "bm1");
252 syncable::MutableEntry e1(&trans, syncable::GET_BY_HANDLE, bm1);
253
254 e1.PutIsDir(true);
255 e1.PutServerIsDir(true);
256
257 sync_pb::EntitySpecifics specifics;
258 sync_pb::BookmarkSpecifics* bm_specifics = specifics.mutable_bookmark();
259 bm_specifics->set_url("http://www.chrome.com");
260 bm_specifics->set_title("Chrome");
261 sync_pb::MetaInfo* meta_info = bm_specifics->add_meta_info();
262 meta_info->set_key("K");
263 meta_info->set_value("V");
264 e1.PutSpecifics(specifics);
265
266 e1.PutIsDel(true);
267 e1.PutIsUnsynced(true);
268 }
269
270 DirectoryTypeDebugInfoEmitter emitter(BOOKMARKS, &type_observers_);
271 scoped_ptr<DirectoryCommitContribution> bm_cc(
272 DirectoryCommitContribution::Build(dir(), BOOKMARKS, 25, &emitter));
273 ASSERT_TRUE(bm_cc);
274
275 sync_pb::ClientToServerMessage message;
276 bm_cc->AddToCommitMessage(&message);
277
278 const sync_pb::CommitMessage& commit_message = message.commit();
279 ASSERT_EQ(1, commit_message.entries_size());
280
281 const sync_pb::SyncEntity& entity = commit_message.entries(0);
282 EXPECT_TRUE(entity.has_folder());
283 ASSERT_TRUE(entity.specifics().has_bookmark());
284 ASSERT_EQ(1, entity.specifics().bookmark().meta_info_size());
285 EXPECT_EQ("K", entity.specifics().bookmark().meta_info(0).key());
286 EXPECT_EQ("V", entity.specifics().bookmark().meta_info(0).value());
287
288 bm_cc->CleanUp();
289 }
290
291 // Test that bookmarks support hierarchy.
TEST_F(DirectoryCommitContributionTest,HierarchySupport_Bookmark)292 TEST_F(DirectoryCommitContributionTest, HierarchySupport_Bookmark) {
293
294 // Create a normal-looking bookmark item.
295 int64 bm1;
296 {
297 syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
298 bm1 = CreateSyncedItem(&trans, BOOKMARKS, "bm1");
299 syncable::MutableEntry e(&trans, syncable::GET_BY_HANDLE, bm1);
300
301 sync_pb::EntitySpecifics specifics;
302 sync_pb::BookmarkSpecifics* bm_specifics = specifics.mutable_bookmark();
303 bm_specifics->set_url("http://www.chrome.com");
304 bm_specifics->set_title("Chrome");
305 e.PutSpecifics(specifics);
306
307 e.PutIsDel(false);
308 e.PutIsUnsynced(true);
309
310 EXPECT_TRUE(e.ShouldMaintainHierarchy());
311 }
312
313 DirectoryTypeDebugInfoEmitter emitter(BOOKMARKS, &type_observers_);
314 scoped_ptr<DirectoryCommitContribution> bm_cc(
315 DirectoryCommitContribution::Build(dir(), BOOKMARKS, 25, &emitter));
316
317 sync_pb::ClientToServerMessage message;
318 bm_cc->AddToCommitMessage(&message);
319 const sync_pb::CommitMessage& commit_message = message.commit();
320 bm_cc->CleanUp();
321
322 ASSERT_EQ(1, commit_message.entries_size());
323 EXPECT_TRUE(commit_message.entries(0).has_parent_id_string());
324 EXPECT_FALSE(commit_message.entries(0).parent_id_string().empty());
325 }
326
327 // Test that preferences do not support hierarchy.
TEST_F(DirectoryCommitContributionTest,HierarchySupport_Preferences)328 TEST_F(DirectoryCommitContributionTest, HierarchySupport_Preferences) {
329 // Create a normal-looking prefs item.
330 int64 pref1;
331 {
332 syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
333 pref1 = CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
334 syncable::MutableEntry e(&trans, syncable::GET_BY_HANDLE, pref1);
335
336 EXPECT_FALSE(e.ShouldMaintainHierarchy());
337 }
338
339 DirectoryTypeDebugInfoEmitter emitter(PREFERENCES, &type_observers_);
340 scoped_ptr<DirectoryCommitContribution> pref_cc(
341 DirectoryCommitContribution::Build(dir(), PREFERENCES, 25, &emitter));
342
343 sync_pb::ClientToServerMessage message;
344 pref_cc->AddToCommitMessage(&message);
345 const sync_pb::CommitMessage& commit_message = message.commit();
346 pref_cc->CleanUp();
347
348 ASSERT_EQ(1, commit_message.entries_size());
349 EXPECT_FALSE(commit_message.entries(0).has_parent_id_string());
350 EXPECT_TRUE(commit_message.entries(0).parent_id_string().empty());
351 }
352
AddAttachment(sync_pb::AttachmentMetadata * metadata,bool is_on_server)353 void AddAttachment(sync_pb::AttachmentMetadata* metadata, bool is_on_server) {
354 sync_pb::AttachmentMetadataRecord record;
355 *record.mutable_id() = CreateAttachmentIdProto();
356 record.set_is_on_server(is_on_server);
357 *metadata->add_record() = record;
358 }
359
360 // Creates some unsynced items, pretends to commit them, and hands back a
361 // specially crafted response to the syncer in order to test commit response
362 // processing. The response simulates a succesful commit scenario.
TEST_F(DirectoryCommitContributionTest,ProcessCommitResponse)363 TEST_F(DirectoryCommitContributionTest, ProcessCommitResponse) {
364 int64 pref1_handle;
365 int64 pref2_handle;
366 int64 ext1_handle;
367 {
368 syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
369 pref1_handle = CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
370 pref2_handle = CreateUnsyncedItem(&trans, PREFERENCES, "pref2");
371 ext1_handle = CreateUnsyncedItem(&trans, EXTENSIONS, "extension1");
372 }
373
374 DirectoryTypeDebugInfoEmitter emitter1(PREFERENCES, &type_observers_);
375 DirectoryTypeDebugInfoEmitter emitter2(EXTENSIONS, &type_observers_);
376 scoped_ptr<DirectoryCommitContribution> pref_cc(
377 DirectoryCommitContribution::Build(dir(), PREFERENCES, 25, &emitter1));
378 scoped_ptr<DirectoryCommitContribution> ext_cc(
379 DirectoryCommitContribution::Build(dir(), EXTENSIONS, 25, &emitter2));
380
381 sync_pb::ClientToServerMessage message;
382 pref_cc->AddToCommitMessage(&message);
383 ext_cc->AddToCommitMessage(&message);
384
385 const sync_pb::CommitMessage& commit_message = message.commit();
386 ASSERT_EQ(3, commit_message.entries_size());
387
388 sync_pb::ClientToServerResponse response;
389 for (int i = 0; i < commit_message.entries_size(); ++i) {
390 sync_pb::SyncEntity entity = commit_message.entries(i);
391 sync_pb::CommitResponse_EntryResponse* entry_response =
392 response.mutable_commit()->add_entryresponse();
393 CreateSuccessfulCommitResponse(entity, entry_response);
394 }
395
396 sessions::StatusController status;
397
398 // Process these in reverse order. Just because we can.
399 ext_cc->ProcessCommitResponse(response, &status);
400 pref_cc->ProcessCommitResponse(response, &status);
401
402 {
403 syncable::ReadTransaction trans(FROM_HERE, dir());
404 syncable::Entry p1(&trans, syncable::GET_BY_HANDLE, pref1_handle);
405 EXPECT_TRUE(p1.GetId().ServerKnows());
406 EXPECT_FALSE(p1.GetSyncing());
407 EXPECT_LT(0, p1.GetServerVersion());
408
409 syncable::Entry p2(&trans, syncable::GET_BY_HANDLE, pref2_handle);
410 EXPECT_TRUE(p2.GetId().ServerKnows());
411 EXPECT_FALSE(p2.GetSyncing());
412 EXPECT_LT(0, p2.GetServerVersion());
413
414 syncable::Entry e1(&trans, syncable::GET_BY_HANDLE, ext1_handle);
415 EXPECT_TRUE(e1.GetId().ServerKnows());
416 EXPECT_FALSE(e1.GetSyncing());
417 EXPECT_LT(0, e1.GetServerVersion());
418 }
419
420 pref_cc->CleanUp();
421 ext_cc->CleanUp();
422 }
423
424 // Creates some unsynced items with attachments and verifies that only items
425 // where all attachments have been uploaded to the server are eligible to be
426 // committed.
TEST_F(DirectoryCommitContributionTest,ProcessCommitResponseWithAttachments)427 TEST_F(DirectoryCommitContributionTest, ProcessCommitResponseWithAttachments) {
428 int64 art1_handle;
429 int64 art2_handle;
430 int64 art3_handle;
431 {
432 syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
433
434 // art1 has two attachments, both have been uploaded to the server. art1 is
435 // eligible to be committed.
436 sync_pb::AttachmentMetadata art1_attachments;
437 AddAttachment(&art1_attachments, true /* is_on_server */);
438 AddAttachment(&art1_attachments, true /* is_on_server */);
439 art1_handle = CreateUnsyncedItemWithAttachments(
440 &trans, ARTICLES, "art1", art1_attachments);
441
442 // art2 has two attachments, one of which has been uploaded to the
443 // server. art2 is not eligible to be committed.
444 sync_pb::AttachmentMetadata art2_attachments;
445 AddAttachment(&art2_attachments, false /* is_on_server */);
446 AddAttachment(&art2_attachments, true /* is_on_server */);
447 art2_handle = CreateUnsyncedItemWithAttachments(
448 &trans, ARTICLES, "art2", art2_attachments);
449
450 // art3 has two attachments, neither of which have been uploaded to the
451 // server. art2 is not eligible to be committed.
452 sync_pb::AttachmentMetadata art3_attachments;
453 AddAttachment(&art3_attachments, false /* is_on_server */);
454 AddAttachment(&art3_attachments, false /* is_on_server */);
455 art3_handle = CreateUnsyncedItemWithAttachments(
456 &trans, ARTICLES, "art3", art3_attachments);
457 }
458
459 DirectoryTypeDebugInfoEmitter emitter(ARTICLES, &type_observers_);
460 scoped_ptr<DirectoryCommitContribution> art_cc(
461 DirectoryCommitContribution::Build(dir(), ARTICLES, 25, &emitter));
462
463 // Only art1 is ready.
464 EXPECT_EQ(1U, art_cc->GetNumEntries());
465
466 sync_pb::ClientToServerMessage message;
467 art_cc->AddToCommitMessage(&message);
468
469 const sync_pb::CommitMessage& commit_message = message.commit();
470 ASSERT_EQ(1, commit_message.entries_size());
471
472 sync_pb::ClientToServerResponse response;
473 for (int i = 0; i < commit_message.entries_size(); ++i) {
474 sync_pb::SyncEntity entity = commit_message.entries(i);
475 sync_pb::CommitResponse_EntryResponse* entry_response =
476 response.mutable_commit()->add_entryresponse();
477 CreateSuccessfulCommitResponse(entity, entry_response);
478 }
479
480 sessions::StatusController status;
481 art_cc->ProcessCommitResponse(response, &status);
482 {
483 syncable::ReadTransaction trans(FROM_HERE, dir());
484
485 syncable::Entry a1(&trans, syncable::GET_BY_HANDLE, art1_handle);
486 EXPECT_TRUE(a1.GetId().ServerKnows());
487 EXPECT_FALSE(a1.GetSyncing());
488 EXPECT_LT(0, a1.GetServerVersion());
489
490 syncable::Entry a2(&trans, syncable::GET_BY_HANDLE, art2_handle);
491 EXPECT_FALSE(a2.GetId().ServerKnows());
492 EXPECT_FALSE(a2.GetSyncing());
493 EXPECT_EQ(0, a2.GetServerVersion());
494
495 syncable::Entry a3(&trans, syncable::GET_BY_HANDLE, art3_handle);
496 EXPECT_FALSE(a3.GetId().ServerKnows());
497 EXPECT_FALSE(a3.GetSyncing());
498 EXPECT_EQ(0, a3.GetServerVersion());
499 }
500
501 art_cc->CleanUp();
502 }
503
504 } // namespace syncer
505