text: // Copyright 2014 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  #include "sync/engine/directory_commit_contribution.h"
  
  #include "base/message_loop/message_loop.h"
  #include "sync/internal_api/public/base/attachment_id_proto.h"
  #include "sync/sessions/status_controller.h"
  #include "sync/syncable/entry.h"
  #include "sync/syncable/mutable_entry.h"
  #include "sync/syncable/syncable_read_transaction.h"
  #include "sync/syncable/syncable_write_transaction.h"
  #include "sync/test/engine/test_directory_setter_upper.h"
  #include "sync/test/engine/test_id_factory.h"
  #include "sync/test/engine/test_syncable_utils.h"
  #include "testing/gtest/include/gtest/gtest.h"
  
  namespace syncer {
  
  class DirectoryCommitContributionTest : public ::testing::Test {
   public:
    virtual void SetUp() override {
      dir_maker_.SetUp();
  
      syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
      CreateTypeRoot(&trans, dir(), PREFERENCES);
      CreateTypeRoot(&trans, dir(), EXTENSIONS);
      CreateTypeRoot(&trans, dir(), ARTICLES);
      CreateTypeRoot(&trans, dir(), BOOKMARKS);
    }
  
    virtual void TearDown() override {
      dir_maker_.TearDown();
    }
  
   protected:
    int64 CreateUnsyncedItemWithAttachments(
        syncable::WriteTransaction* trans,
        ModelType type,
        const std::string& tag,
        const sync_pb::AttachmentMetadata& attachment_metadata) {
      syncable::Entry parent_entry(trans, syncable::GET_TYPE_ROOT, type);
      syncable::MutableEntry entry(
          trans,
          syncable::CREATE,
          type,
          parent_entry.GetId(),
          tag);
      if (attachment_metadata.record_size() > 0) {
        entry.PutAttachmentMetadata(attachment_metadata);
      }
      entry.PutIsUnsynced(true);
      return entry.GetMetahandle();
    }
  
    int64 CreateUnsyncedItem(syncable::WriteTransaction* trans,
                             ModelType type,
                             const std::string& tag) {
      return CreateUnsyncedItemWithAttachments(
          trans, type, tag, sync_pb::AttachmentMetadata());
    }
  
    int64 CreateSyncedItem(syncable::WriteTransaction* trans,
                           ModelType type,
                           const std::string& tag) {
      syncable::Entry parent_entry(trans, syncable::GET_TYPE_ROOT, type);
      syncable::MutableEntry entry(
          trans,
          syncable::CREATE,
          type,
          parent_entry.GetId(),
          tag);
  
      entry.PutId(syncable::Id::CreateFromServerId(
          id_factory_.NewServerId().GetServerId()));
      entry.PutBaseVersion(10);
      entry.PutServerVersion(10);
      entry.PutIsUnappliedUpdate(false);
      entry.PutIsUnsynced(false);
      entry.PutIsDel(false);
      entry.PutServerIsDel(false);
  
      return entry.GetMetahandle();
    }
  
    void CreateSuccessfulCommitResponse(
        const sync_pb::SyncEntity& entity,
        sync_pb::CommitResponse::EntryResponse* response) {
      response->set_response_type(sync_pb::CommitResponse::SUCCESS);
      response->set_non_unique_name(entity.name());
      response->set_version(entity.version() + 1);
      response->set_parent_id_string(entity.parent_id_string());
  
      if (entity.id_string()[0] == '-')  // Look for the - in 'c-1234' style IDs.
        response->set_id_string(id_factory_.NewServerId().GetServerId());
      else
        response->set_id_string(entity.id_string());
    }
  
    syncable::Directory* dir() {
      return dir_maker_.directory();
    }
  
    TestIdFactory id_factory_;
  
    // Used in construction of DirectoryTypeDebugInfoEmitters.
    ObserverList<TypeDebugInfoObserver> type_observers_;
  
   private:
    base::MessageLoop loop_;  // Neeed to initialize the directory.
    TestDirectorySetterUpper dir_maker_;
  };
  
  // Verify that the DirectoryCommitContribution contains only entries of its
  // specified type.
  TEST_F(DirectoryCommitContributionTest, GatherByTypes) {
    int64 pref1;
    {
      syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
      pref1 = CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
      CreateUnsyncedItem(&trans, PREFERENCES, "pref2");
      CreateUnsyncedItem(&trans, EXTENSIONS, "extension1");
    }
  
    DirectoryTypeDebugInfoEmitter emitter(PREFERENCES, &type_observers_);
    scoped_ptr<DirectoryCommitContribution> cc(
        DirectoryCommitContribution::Build(dir(), PREFERENCES, 5, &emitter));
    ASSERT_EQ(2U, cc->GetNumEntries());
  
    const std::vector<int64>& metahandles = cc->metahandles_;
    EXPECT_TRUE(std::find(metahandles.begin(), metahandles.end(), pref1) !=
                metahandles.end());
    EXPECT_TRUE(std::find(metahandles.begin(), metahandles.end(), pref1) !=
                metahandles.end());
  
    cc->CleanUp();
  }
  
  // Verify that the DirectoryCommitContributionTest builder function
  // truncates if necessary.
  TEST_F(DirectoryCommitContributionTest, GatherAndTruncate) {
    int64 pref1;
    int64 pref2;
    {
      syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
      pref1 = CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
      pref2 = CreateUnsyncedItem(&trans, PREFERENCES, "pref2");
      CreateUnsyncedItem(&trans, EXTENSIONS, "extension1");
    }
  
    DirectoryTypeDebugInfoEmitter emitter(PREFERENCES, &type_observers_);
    scoped_ptr<DirectoryCommitContribution> cc(
        DirectoryCommitContribution::Build(dir(), PREFERENCES, 1, &emitter));
    ASSERT_EQ(1U, cc->GetNumEntries());
  
    int64 only_metahandle = cc->metahandles_[0];
    EXPECT_TRUE(only_metahandle == pref1 || only_metahandle == pref2);
  
    cc->CleanUp();
  }
  
  // Sanity check for building commits from DirectoryCommitContributions.
  // This test makes two CommitContribution objects of different types and uses
  // them to initialize a commit message.  Then it checks that the contents of the
  // commit message match those of the directory they came from.
  TEST_F(DirectoryCommitContributionTest, PrepareCommit) {
    {
      syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
      CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
      CreateUnsyncedItem(&trans, PREFERENCES, "pref2");
      CreateUnsyncedItem(&trans, EXTENSIONS, "extension1");
    }
  
    DirectoryTypeDebugInfoEmitter emitter1(PREFERENCES, &type_observers_);
    DirectoryTypeDebugInfoEmitter emitter2(EXTENSIONS, &type_observers_);
    scoped_ptr<DirectoryCommitContribution> pref_cc(
        DirectoryCommitContribution::Build(dir(), PREFERENCES, 25, &emitter1));
    scoped_ptr<DirectoryCommitContribution> ext_cc(
        DirectoryCommitContribution::Build(dir(), EXTENSIONS, 25, &emitter2));
  
    sync_pb::ClientToServerMessage message;
    pref_cc->AddToCommitMessage(&message);
    ext_cc->AddToCommitMessage(&message);
  
    const sync_pb::CommitMessage& commit_message = message.commit();
  
    std::set<syncable::Id> ids_for_commit;
    ASSERT_EQ(3, commit_message.entries_size());
    for (int i = 0; i < commit_message.entries_size(); ++i) {
      const sync_pb::SyncEntity& entity = commit_message.entries(i);
      // The entities in this test have client-style IDs since they've never been
      // committed before, so we must use CreateFromClientString to re-create them
      // from the commit message.
      ids_for_commit.insert(syncable::Id::CreateFromClientString(
              entity.id_string()));
    }
  
    ASSERT_EQ(3U, ids_for_commit.size());
    {
      syncable::ReadTransaction trans(FROM_HERE, dir());
      for (std::set<syncable::Id>::iterator it = ids_for_commit.begin();
           it != ids_for_commit.end(); ++it) {
        SCOPED_TRACE(it->value());
        syncable::Entry entry(&trans, syncable::GET_BY_ID, *it);
        ASSERT_TRUE(entry.good());
        EXPECT_TRUE(entry.GetSyncing());
      }
    }
  
    pref_cc->CleanUp();
    ext_cc->CleanUp();
  }
  
  // Check that deletion requests include a model type.
  // This was not always the case, but was implemented to allow us to loosen some
  // other restrictions in the protocol.
  TEST_F(DirectoryCommitContributionTest, DeletedItemsWithSpecifics) {
    int64 pref1;
    {
      syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
      pref1 = CreateSyncedItem(&trans, PREFERENCES, "pref1");
      syncable::MutableEntry e1(&trans, syncable::GET_BY_HANDLE, pref1);
      e1.PutIsDel(true);
      e1.PutIsUnsynced(true);
    }
  
    DirectoryTypeDebugInfoEmitter emitter(PREFERENCES, &type_observers_);
    scoped_ptr<DirectoryCommitContribution> pref_cc(
        DirectoryCommitContribution::Build(dir(), PREFERENCES, 25, &emitter));
    ASSERT_TRUE(pref_cc);
  
    sync_pb::ClientToServerMessage message;
    pref_cc->AddToCommitMessage(&message);
  
    const sync_pb::CommitMessage& commit_message = message.commit();
    ASSERT_EQ(1, commit_message.entries_size());
    EXPECT_TRUE(
        commit_message.entries(0).specifics().has_preference());
  
    pref_cc->CleanUp();
  }
  
  // As ususal, bookmarks are special.  Bookmark deletion is special.
  // Deleted bookmarks include a valid "is folder" bit and their full specifics
  // (especially the meta info, which is what server really wants).
  TEST_F(DirectoryCommitContributionTest, DeletedBookmarksWithSpecifics) {
    int64 bm1;
    {
      syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
      bm1 = CreateSyncedItem(&trans, BOOKMARKS, "bm1");
      syncable::MutableEntry e1(&trans, syncable::GET_BY_HANDLE, bm1);
  
      e1.PutIsDir(true);
      e1.PutServerIsDir(true);
  
      sync_pb::EntitySpecifics specifics;
      sync_pb::BookmarkSpecifics* bm_specifics = specifics.mutable_bookmark();
      bm_specifics->set_url("http://www.chrome.com");
      bm_specifics->set_title("Chrome");
      sync_pb::MetaInfo* meta_info = bm_specifics->add_meta_info();
      meta_info->set_key("K");
      meta_info->set_value("V");
      e1.PutSpecifics(specifics);
  
      e1.PutIsDel(true);
      e1.PutIsUnsynced(true);
    }
  
    DirectoryTypeDebugInfoEmitter emitter(BOOKMARKS, &type_observers_);
    scoped_ptr<DirectoryCommitContribution> bm_cc(
        DirectoryCommitContribution::Build(dir(), BOOKMARKS, 25, &emitter));
    ASSERT_TRUE(bm_cc);
  
    sync_pb::ClientToServerMessage message;
    bm_cc->AddToCommitMessage(&message);
  
    const sync_pb::CommitMessage& commit_message = message.commit();
    ASSERT_EQ(1, commit_message.entries_size());
  
    const sync_pb::SyncEntity& entity = commit_message.entries(0);
    EXPECT_TRUE(entity.has_folder());
    ASSERT_TRUE(entity.specifics().has_bookmark());
    ASSERT_EQ(1, entity.specifics().bookmark().meta_info_size());
    EXPECT_EQ("K", entity.specifics().bookmark().meta_info(0).key());
    EXPECT_EQ("V", entity.specifics().bookmark().meta_info(0).value());
  
    bm_cc->CleanUp();
  }
  
  // Test that bookmarks support hierarchy.
  TEST_F(DirectoryCommitContributionTest, HierarchySupport_Bookmark) {
  
    // Create a normal-looking bookmark item.
    int64 bm1;
    {
      syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
      bm1 = CreateSyncedItem(&trans, BOOKMARKS, "bm1");
      syncable::MutableEntry e(&trans, syncable::GET_BY_HANDLE, bm1);
  
      sync_pb::EntitySpecifics specifics;
      sync_pb::BookmarkSpecifics* bm_specifics = specifics.mutable_bookmark();
      bm_specifics->set_url("http://www.chrome.com");
      bm_specifics->set_title("Chrome");
      e.PutSpecifics(specifics);
  
      e.PutIsDel(false);
      e.PutIsUnsynced(true);
  
      EXPECT_TRUE(e.ShouldMaintainHierarchy());
    }
  
    DirectoryTypeDebugInfoEmitter emitter(BOOKMARKS, &type_observers_);
    scoped_ptr<DirectoryCommitContribution> bm_cc(
        DirectoryCommitContribution::Build(dir(), BOOKMARKS, 25, &emitter));
  
    sync_pb::ClientToServerMessage message;
    bm_cc->AddToCommitMessage(&message);
    const sync_pb::CommitMessage& commit_message = message.commit();
    bm_cc->CleanUp();
  
    ASSERT_EQ(1, commit_message.entries_size());
    EXPECT_TRUE(commit_message.entries(0).has_parent_id_string());
    EXPECT_FALSE(commit_message.entries(0).parent_id_string().empty());
  }
  
  // Test that preferences do not support hierarchy.
  TEST_F(DirectoryCommitContributionTest, HierarchySupport_Preferences) {
    // Create a normal-looking prefs item.
    int64 pref1;
    {
      syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
      pref1 = CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
      syncable::MutableEntry e(&trans, syncable::GET_BY_HANDLE, pref1);
  
      EXPECT_FALSE(e.ShouldMaintainHierarchy());
    }
  
    DirectoryTypeDebugInfoEmitter emitter(PREFERENCES, &type_observers_);
    scoped_ptr<DirectoryCommitContribution> pref_cc(
        DirectoryCommitContribution::Build(dir(), PREFERENCES, 25, &emitter));
  
    sync_pb::ClientToServerMessage message;
    pref_cc->AddToCommitMessage(&message);
    const sync_pb::CommitMessage& commit_message = message.commit();
    pref_cc->CleanUp();
  
    ASSERT_EQ(1, commit_message.entries_size());
    EXPECT_FALSE(commit_message.entries(0).has_parent_id_string());
    EXPECT_TRUE(commit_message.entries(0).parent_id_string().empty());
  }
  
  void AddAttachment(sync_pb::AttachmentMetadata* metadata, bool is_on_server) {
    sync_pb::AttachmentMetadataRecord record;
    *record.mutable_id() = CreateAttachmentIdProto();
    record.set_is_on_server(is_on_server);
    *metadata->add_record() = record;
  }
  
  // Creates some unsynced items, pretends to commit them, and hands back a
  // specially crafted response to the syncer in order to test commit response
  // processing.  The response simulates a succesful commit scenario.
  TEST_F(DirectoryCommitContributionTest, ProcessCommitResponse) {
    int64 pref1_handle;
    int64 pref2_handle;
    int64 ext1_handle;
    {
      syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
      pref1_handle = CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
      pref2_handle = CreateUnsyncedItem(&trans, PREFERENCES, "pref2");
      ext1_handle = CreateUnsyncedItem(&trans, EXTENSIONS, "extension1");
    }
  
    DirectoryTypeDebugInfoEmitter emitter1(PREFERENCES, &type_observers_);
    DirectoryTypeDebugInfoEmitter emitter2(EXTENSIONS, &type_observers_);
    scoped_ptr<DirectoryCommitContribution> pref_cc(
        DirectoryCommitContribution::Build(dir(), PREFERENCES, 25, &emitter1));
    scoped_ptr<DirectoryCommitContribution> ext_cc(
        DirectoryCommitContribution::Build(dir(), EXTENSIONS, 25, &emitter2));
  
    sync_pb::ClientToServerMessage message;
    pref_cc->AddToCommitMessage(&message);
    ext_cc->AddToCommitMessage(&message);
  
    const sync_pb::CommitMessage& commit_message = message.commit();
    ASSERT_EQ(3, commit_message.entries_size());
  
    sync_pb::ClientToServerResponse response;
    for (int i = 0; i < commit_message.entries_size(); ++i) {
      sync_pb::SyncEntity entity = commit_message.entries(i);
      sync_pb::CommitResponse_EntryResponse* entry_response =
          response.mutable_commit()->add_entryresponse();
      CreateSuccessfulCommitResponse(entity, entry_response);
    }
  
    sessions::StatusController status;
  
    // Process these in reverse order.  Just because we can.
    ext_cc->ProcessCommitResponse(response, &status);
    pref_cc->ProcessCommitResponse(response, &status);
  
    {
      syncable::ReadTransaction trans(FROM_HERE, dir());
      syncable::Entry p1(&trans, syncable::GET_BY_HANDLE, pref1_handle);
      EXPECT_TRUE(p1.GetId().ServerKnows());
      EXPECT_FALSE(p1.GetSyncing());
      EXPECT_LT(0, p1.GetServerVersion());
  
      syncable::Entry p2(&trans, syncable::GET_BY_HANDLE, pref2_handle);
      EXPECT_TRUE(p2.GetId().ServerKnows());
      EXPECT_FALSE(p2.GetSyncing());
      EXPECT_LT(0, p2.GetServerVersion());
  
      syncable::Entry e1(&trans, syncable::GET_BY_HANDLE, ext1_handle);
      EXPECT_TRUE(e1.GetId().ServerKnows());
      EXPECT_FALSE(e1.GetSyncing());
      EXPECT_LT(0, e1.GetServerVersion());
    }
  
    pref_cc->CleanUp();
    ext_cc->CleanUp();
  }
  
  // Creates some unsynced items with attachments and verifies that only items
  // where all attachments have been uploaded to the server are eligible to be
  // committed.
  TEST_F(DirectoryCommitContributionTest, ProcessCommitResponseWithAttachments) {
    int64 art1_handle;
    int64 art2_handle;
    int64 art3_handle;
    {
      syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
  
      // art1 has two attachments, both have been uploaded to the server.  art1 is
      // eligible to be committed.
      sync_pb::AttachmentMetadata art1_attachments;
      AddAttachment(&art1_attachments, true /* is_on_server */);
      AddAttachment(&art1_attachments, true /* is_on_server */);
      art1_handle = CreateUnsyncedItemWithAttachments(
          &trans, ARTICLES, "art1", art1_attachments);
  
      // art2 has two attachments, one of which has been uploaded to the
      // server. art2 is not eligible to be committed.
      sync_pb::AttachmentMetadata art2_attachments;
      AddAttachment(&art2_attachments, false /* is_on_server */);
      AddAttachment(&art2_attachments, true /* is_on_server */);
      art2_handle = CreateUnsyncedItemWithAttachments(
          &trans, ARTICLES, "art2", art2_attachments);
  
      // art3 has two attachments, neither of which have been uploaded to the
      // server. art2 is not eligible to be committed.
      sync_pb::AttachmentMetadata art3_attachments;
      AddAttachment(&art3_attachments, false /* is_on_server */);
      AddAttachment(&art3_attachments, false /* is_on_server */);
      art3_handle = CreateUnsyncedItemWithAttachments(
          &trans, ARTICLES, "art3", art3_attachments);
    }
  
    DirectoryTypeDebugInfoEmitter emitter(ARTICLES, &type_observers_);
    scoped_ptr<DirectoryCommitContribution> art_cc(
        DirectoryCommitContribution::Build(dir(), ARTICLES, 25, &emitter));
  
    // Only art1 is ready.
    EXPECT_EQ(1U, art_cc->GetNumEntries());
  
    sync_pb::ClientToServerMessage message;
    art_cc->AddToCommitMessage(&message);
  
    const sync_pb::CommitMessage& commit_message = message.commit();
    ASSERT_EQ(1, commit_message.entries_size());
  
    sync_pb::ClientToServerResponse response;
    for (int i = 0; i < commit_message.entries_size(); ++i) {
      sync_pb::SyncEntity entity = commit_message.entries(i);
      sync_pb::CommitResponse_EntryResponse* entry_response =
          response.mutable_commit()->add_entryresponse();
      CreateSuccessfulCommitResponse(entity, entry_response);
    }
  
    sessions::StatusController status;
    art_cc->ProcessCommitResponse(response, &status);
    {
      syncable::ReadTransaction trans(FROM_HERE, dir());
  
      syncable::Entry a1(&trans, syncable::GET_BY_HANDLE, art1_handle);
      EXPECT_TRUE(a1.GetId().ServerKnows());
      EXPECT_FALSE(a1.GetSyncing());
      EXPECT_LT(0, a1.GetServerVersion());
  
      syncable::Entry a2(&trans, syncable::GET_BY_HANDLE, art2_handle);
      EXPECT_FALSE(a2.GetId().ServerKnows());
      EXPECT_FALSE(a2.GetSyncing());
      EXPECT_EQ(0, a2.GetServerVersion());
  
      syncable::Entry a3(&trans, syncable::GET_BY_HANDLE, art3_handle);
      EXPECT_FALSE(a3.GetId().ServerKnows());
      EXPECT_FALSE(a3.GetSyncing());
      EXPECT_EQ(0, a3.GetServerVersion());
    }
  
    art_cc->CleanUp();
  }
  
  }  // namespace syncer
  