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
19namespace syncer {
20
21class DirectoryCommitContributionTest : public ::testing::Test {
22 public:
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
33  virtual void TearDown() OVERRIDE {
34    dir_maker_.TearDown();
35  }
36
37 protected:
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
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
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
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
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.
117TEST_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.
142TEST_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.
167TEST_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.
218TEST_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).
247TEST_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.
292TEST_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.
328TEST_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
353void 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.
363TEST_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.
427TEST_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