blob: 41d53b1d5269cf5f67d7017b94765b95c9473d55 [file] [log] [blame]
// 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 "components/sync/driver/generic_change_processor.h"
#include <stddef.h>
#include <utility>
#include "base/memory/ptr_util.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_task_environment.h"
#include "components/sync/base/model_type.h"
#include "components/sync/device_info/local_device_info_provider.h"
#include "components/sync/driver/fake_sync_client.h"
#include "components/sync/driver/sync_api_component_factory.h"
#include "components/sync/engine/attachments/fake_attachment_downloader.h"
#include "components/sync/engine/attachments/fake_attachment_uploader.h"
#include "components/sync/engine/sync_encryption_handler.h"
#include "components/sync/model/attachments/attachment_id.h"
#include "components/sync/model/data_type_error_handler_mock.h"
#include "components/sync/model/fake_syncable_service.h"
#include "components/sync/model/sync_change.h"
#include "components/sync/syncable/read_node.h"
#include "components/sync/syncable/read_transaction.h"
#include "components/sync/syncable/test_user_share.h"
#include "components/sync/syncable/user_share.h"
#include "components/sync/syncable/write_node.h"
#include "components/sync/syncable/write_transaction.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace syncer {
namespace {
// A mock that keeps track of attachments passed to UploadAttachments.
class MockAttachmentService : public AttachmentService {
public:
MockAttachmentService() {}
~MockAttachmentService() override {}
void GetOrDownloadAttachments(
const AttachmentIdList& attachment_ids,
const GetOrDownloadCallback& callback) override {}
void UploadAttachments(const AttachmentIdList& attachment_ids) override {
attachment_id_lists_.push_back(attachment_ids);
}
std::vector<AttachmentIdList>* attachment_id_lists() {
return &attachment_id_lists_;
}
private:
std::vector<AttachmentIdList> attachment_id_lists_;
};
// MockSyncApiComponentFactory needed to initialize GenericChangeProcessor and
// pass MockAttachmentService to it.
class MockSyncApiComponentFactory : public SyncApiComponentFactory {
public:
MockSyncApiComponentFactory() {}
// SyncApiComponentFactory implementation.
void RegisterDataTypes(
SyncService* sync_service,
const RegisterDataTypesMethod& register_platform_types_method) override {}
DataTypeManager* CreateDataTypeManager(
ModelTypeSet initial_types,
const WeakHandle<DataTypeDebugInfoListener>& debug_info_listener,
const DataTypeController::TypeMap* controllers,
const DataTypeEncryptionHandler* encryption_handler,
ModelTypeConfigurer* configurer,
DataTypeManagerObserver* observer) override {
return nullptr;
};
SyncEngine* CreateSyncEngine(const std::string& name,
invalidation::InvalidationService* invalidator,
const base::WeakPtr<SyncPrefs>& sync_prefs,
const base::FilePath& sync_folder) override {
return nullptr;
}
std::unique_ptr<LocalDeviceInfoProvider> CreateLocalDeviceInfoProvider()
override {
return nullptr;
}
SyncComponents CreateBookmarkSyncComponents(
SyncService* sync_service,
std::unique_ptr<DataTypeErrorHandler> error_handler) override {
return SyncComponents(nullptr, nullptr);
}
std::unique_ptr<AttachmentService> CreateAttachmentService(
std::unique_ptr<AttachmentStoreForSync> attachment_store,
const UserShare& user_share,
const std::string& store_birthday,
ModelType model_type,
AttachmentService::Delegate* delegate) override {
auto attachment_service = base::MakeUnique<MockAttachmentService>();
// GenericChangeProcessor takes ownership of the AttachmentService, but we
// need to have a pointer to it so we can see that it was used properly.
// Take a pointer and trust that GenericChangeProcessor does not prematurely
// destroy it.
mock_attachment_service_ = attachment_service.get();
return std::move(attachment_service);
}
MockAttachmentService* GetMockAttachmentService() {
return mock_attachment_service_;
}
private:
MockAttachmentService* mock_attachment_service_;
};
class SyncGenericChangeProcessorTest : public testing::Test {
public:
// Most test cases will use this type. For those that need a
// GenericChangeProcessor for a different type, use |InitializeForType|.
static const ModelType kType = PREFERENCES;
SyncGenericChangeProcessorTest()
: scoped_task_environment_(
base::test::ScopedTaskEnvironment::MainThreadType::UI),
syncable_service_ptr_factory_(&fake_syncable_service_),
mock_attachment_service_(nullptr),
sync_client_(&sync_factory_) {}
void SetUp() override {
// Use kType by default, but allow test cases to re-initialize with whatever
// type they choose. Therefore, it's important that all type dependent
// initialization occurs in InitializeForType.
InitializeForType(kType);
}
void TearDown() override {
mock_attachment_service_ = nullptr;
if (test_user_share_) {
test_user_share_->TearDown();
}
}
// Initialize GenericChangeProcessor and related classes for testing with
// model type |type|.
void InitializeForType(ModelType type) {
TearDown();
test_user_share_ = base::MakeUnique<TestUserShare>();
test_user_share_->SetUp();
sync_merge_result_ = base::MakeUnique<SyncMergeResult>(type);
merge_result_ptr_factory_ =
base::MakeUnique<base::WeakPtrFactory<SyncMergeResult>>(
sync_merge_result_.get());
ModelTypeSet types = ProtocolTypes();
for (ModelTypeSet::Iterator iter = types.First(); iter.Good(); iter.Inc()) {
TestUserShare::CreateRoot(iter.Get(), test_user_share_->user_share());
}
test_user_share_->encryption_handler()->Init();
ConstructGenericChangeProcessor(type);
}
void ConstructGenericChangeProcessor(ModelType type) {
std::unique_ptr<AttachmentStore> attachment_store =
AttachmentStore::CreateInMemoryStore();
change_processor_ = base::MakeUnique<GenericChangeProcessor>(
type, base::MakeUnique<DataTypeErrorHandlerMock>(),
syncable_service_ptr_factory_.GetWeakPtr(),
merge_result_ptr_factory_->GetWeakPtr(), test_user_share_->user_share(),
&sync_client_, attachment_store->CreateAttachmentStoreForSync());
mock_attachment_service_ = sync_factory_.GetMockAttachmentService();
}
void BuildChildNodes(ModelType type, int n) {
WriteTransaction trans(FROM_HERE, user_share());
for (int i = 0; i < n; ++i) {
WriteNode node(&trans);
node.InitUniqueByCreation(type, base::StringPrintf("node%05d", i));
}
}
GenericChangeProcessor* change_processor() { return change_processor_.get(); }
UserShare* user_share() { return test_user_share_->user_share(); }
MockAttachmentService* mock_attachment_service() {
return mock_attachment_service_;
}
void RunLoop() {
base::RunLoop run_loop;
run_loop.RunUntilIdle();
}
private:
base::test::ScopedTaskEnvironment scoped_task_environment_;
std::unique_ptr<SyncMergeResult> sync_merge_result_;
std::unique_ptr<base::WeakPtrFactory<SyncMergeResult>>
merge_result_ptr_factory_;
FakeSyncableService fake_syncable_service_;
base::WeakPtrFactory<FakeSyncableService> syncable_service_ptr_factory_;
std::unique_ptr<TestUserShare> test_user_share_;
MockAttachmentService* mock_attachment_service_;
FakeSyncClient sync_client_;
MockSyncApiComponentFactory sync_factory_;
std::unique_ptr<GenericChangeProcessor> change_processor_;
};
// Similar to above, but focused on the method that implements sync/api
// interfaces and is hence exposed to datatypes directly.
TEST_F(SyncGenericChangeProcessorTest, StressGetAllSyncData) {
const int kNumChildNodes = 1000;
const int kRepeatCount = 1;
ASSERT_NO_FATAL_FAILURE(BuildChildNodes(kType, kNumChildNodes));
for (int i = 0; i < kRepeatCount; ++i) {
SyncDataList sync_data = change_processor()->GetAllSyncData(kType);
// Start with a simple test. We can add more in-depth testing later.
EXPECT_EQ(static_cast<size_t>(kNumChildNodes), sync_data.size());
}
}
TEST_F(SyncGenericChangeProcessorTest, SetGetPasswords) {
InitializeForType(PASSWORDS);
const int kNumPasswords = 10;
sync_pb::PasswordSpecificsData password_data;
password_data.set_username_value("user");
sync_pb::EntitySpecifics password_holder;
SyncChangeList change_list;
for (int i = 0; i < kNumPasswords; ++i) {
password_data.set_password_value(base::StringPrintf("password%i", i));
password_holder.mutable_password()
->mutable_client_only_encrypted_data()
->CopyFrom(password_data);
change_list.push_back(
SyncChange(FROM_HERE, SyncChange::ACTION_ADD,
SyncData::CreateLocalData(base::StringPrintf("tag%i", i),
base::StringPrintf("title%i", i),
password_holder)));
}
ASSERT_FALSE(
change_processor()->ProcessSyncChanges(FROM_HERE, change_list).IsSet());
SyncDataList password_list(change_processor()->GetAllSyncData(PASSWORDS));
ASSERT_EQ(password_list.size(), change_list.size());
for (int i = 0; i < kNumPasswords; ++i) {
// Verify the password is returned properly.
ASSERT_TRUE(password_list[i].GetSpecifics().has_password());
ASSERT_TRUE(password_list[i]
.GetSpecifics()
.password()
.has_client_only_encrypted_data());
ASSERT_FALSE(password_list[i].GetSpecifics().password().has_encrypted());
const sync_pb::PasswordSpecificsData& sync_password =
password_list[i].GetSpecifics().password().client_only_encrypted_data();
const sync_pb::PasswordSpecificsData& change_password =
change_list[i]
.sync_data()
.GetSpecifics()
.password()
.client_only_encrypted_data();
ASSERT_EQ(sync_password.password_value(), change_password.password_value());
ASSERT_EQ(sync_password.username_value(), change_password.username_value());
// Verify the raw sync data was stored securely.
ReadTransaction read_transaction(FROM_HERE, user_share());
ReadNode node(&read_transaction);
ASSERT_EQ(
node.InitByClientTagLookup(PASSWORDS, base::StringPrintf("tag%i", i)),
BaseNode::INIT_OK);
ASSERT_EQ(node.GetTitle(), "encrypted");
const sync_pb::EntitySpecifics& raw_specifics = node.GetEntitySpecifics();
ASSERT_TRUE(raw_specifics.has_password());
ASSERT_TRUE(raw_specifics.password().has_encrypted());
ASSERT_FALSE(raw_specifics.password().has_client_only_encrypted_data());
}
}
TEST_F(SyncGenericChangeProcessorTest, UpdatePasswords) {
InitializeForType(PASSWORDS);
const int kNumPasswords = 10;
sync_pb::PasswordSpecificsData password_data;
password_data.set_username_value("user");
sync_pb::EntitySpecifics password_holder;
SyncChangeList change_list;
SyncChangeList change_list2;
for (int i = 0; i < kNumPasswords; ++i) {
password_data.set_password_value(base::StringPrintf("password%i", i));
password_holder.mutable_password()
->mutable_client_only_encrypted_data()
->CopyFrom(password_data);
change_list.push_back(
SyncChange(FROM_HERE, SyncChange::ACTION_ADD,
SyncData::CreateLocalData(base::StringPrintf("tag%i", i),
base::StringPrintf("title%i", i),
password_holder)));
password_data.set_password_value(base::StringPrintf("password_m%i", i));
password_holder.mutable_password()
->mutable_client_only_encrypted_data()
->CopyFrom(password_data);
change_list2.push_back(
SyncChange(FROM_HERE, SyncChange::ACTION_UPDATE,
SyncData::CreateLocalData(base::StringPrintf("tag%i", i),
base::StringPrintf("title_m%i", i),
password_holder)));
}
ASSERT_FALSE(
change_processor()->ProcessSyncChanges(FROM_HERE, change_list).IsSet());
ASSERT_FALSE(
change_processor()->ProcessSyncChanges(FROM_HERE, change_list2).IsSet());
SyncDataList password_list(change_processor()->GetAllSyncData(PASSWORDS));
ASSERT_EQ(password_list.size(), change_list2.size());
for (int i = 0; i < kNumPasswords; ++i) {
// Verify the password is returned properly.
ASSERT_TRUE(password_list[i].GetSpecifics().has_password());
ASSERT_TRUE(password_list[i]
.GetSpecifics()
.password()
.has_client_only_encrypted_data());
ASSERT_FALSE(password_list[i].GetSpecifics().password().has_encrypted());
const sync_pb::PasswordSpecificsData& sync_password =
password_list[i].GetSpecifics().password().client_only_encrypted_data();
const sync_pb::PasswordSpecificsData& change_password =
change_list2[i]
.sync_data()
.GetSpecifics()
.password()
.client_only_encrypted_data();
ASSERT_EQ(sync_password.password_value(), change_password.password_value());
ASSERT_EQ(sync_password.username_value(), change_password.username_value());
// Verify the raw sync data was stored securely.
ReadTransaction read_transaction(FROM_HERE, user_share());
ReadNode node(&read_transaction);
ASSERT_EQ(
node.InitByClientTagLookup(PASSWORDS, base::StringPrintf("tag%i", i)),
BaseNode::INIT_OK);
ASSERT_EQ(node.GetTitle(), "encrypted");
const sync_pb::EntitySpecifics& raw_specifics = node.GetEntitySpecifics();
ASSERT_TRUE(raw_specifics.has_password());
ASSERT_TRUE(raw_specifics.password().has_encrypted());
ASSERT_FALSE(raw_specifics.password().has_client_only_encrypted_data());
}
}
// Verify that attachments on newly added or updated SyncData are passed to the
// AttachmentService.
TEST_F(SyncGenericChangeProcessorTest,
ProcessSyncChanges_AddUpdateWithAttachment) {
std::string tag = "client_tag";
std::string title = "client_title";
sync_pb::EntitySpecifics specifics;
sync_pb::PreferenceSpecifics* pref_specifics = specifics.mutable_preference();
pref_specifics->set_name("test");
AttachmentIdList attachment_ids;
attachment_ids.push_back(AttachmentId::Create(0, 0));
attachment_ids.push_back(AttachmentId::Create(0, 0));
// Add a SyncData with two attachments.
SyncChangeList change_list;
change_list.push_back(SyncChange(FROM_HERE, SyncChange::ACTION_ADD,
SyncData::CreateLocalDataWithAttachments(
tag, title, specifics, attachment_ids)));
ASSERT_FALSE(
change_processor()->ProcessSyncChanges(FROM_HERE, change_list).IsSet());
RunLoop();
// Check that the AttachmentService received the new attachments.
ASSERT_EQ(mock_attachment_service()->attachment_id_lists()->size(), 1U);
const AttachmentIdList& attachments_added =
mock_attachment_service()->attachment_id_lists()->front();
ASSERT_THAT(attachments_added, testing::UnorderedElementsAre(
attachment_ids[0], attachment_ids[1]));
// Update the SyncData, replacing its two attachments with one new attachment.
AttachmentIdList new_attachment_ids;
new_attachment_ids.push_back(AttachmentId::Create(0, 0));
mock_attachment_service()->attachment_id_lists()->clear();
change_list.clear();
change_list.push_back(
SyncChange(FROM_HERE, SyncChange::ACTION_UPDATE,
SyncData::CreateLocalDataWithAttachments(tag, title, specifics,
new_attachment_ids)));
ASSERT_FALSE(
change_processor()->ProcessSyncChanges(FROM_HERE, change_list).IsSet());
RunLoop();
// Check that the AttachmentService received it.
ASSERT_EQ(mock_attachment_service()->attachment_id_lists()->size(), 1U);
const AttachmentIdList& new_attachments_added =
mock_attachment_service()->attachment_id_lists()->front();
ASSERT_THAT(new_attachments_added,
testing::UnorderedElementsAre(new_attachment_ids[0]));
}
// Verify that after attachment is uploaded GenericChangeProcessor updates
// corresponding entries
TEST_F(SyncGenericChangeProcessorTest, AttachmentUploaded) {
std::string tag = "client_tag";
std::string title = "client_title";
sync_pb::EntitySpecifics specifics;
sync_pb::PreferenceSpecifics* pref_specifics = specifics.mutable_preference();
pref_specifics->set_name("test");
AttachmentIdList attachment_ids;
attachment_ids.push_back(AttachmentId::Create(0, 0));
// Add a SyncData with two attachments.
SyncChangeList change_list;
change_list.push_back(SyncChange(FROM_HERE, SyncChange::ACTION_ADD,
SyncData::CreateLocalDataWithAttachments(
tag, title, specifics, attachment_ids)));
ASSERT_FALSE(
change_processor()->ProcessSyncChanges(FROM_HERE, change_list).IsSet());
sync_pb::AttachmentIdProto attachment_id_proto = attachment_ids[0].GetProto();
AttachmentId attachment_id =
AttachmentId::CreateFromProto(attachment_id_proto);
change_processor()->OnAttachmentUploaded(attachment_id);
ReadTransaction read_transaction(FROM_HERE, user_share());
ReadNode node(&read_transaction);
ASSERT_EQ(node.InitByClientTagLookup(kType, tag), BaseNode::INIT_OK);
attachment_ids = node.GetAttachmentIds();
EXPECT_EQ(1U, attachment_ids.size());
}
// Verify that upon construction, all attachments not yet on the server are
// scheduled for upload.
TEST_F(SyncGenericChangeProcessorTest, UploadAllAttachmentsNotOnServer) {
// Create two attachment ids. id2 will be marked as "on server".
AttachmentId id1 = AttachmentId::Create(0, 0);
AttachmentId id2 = AttachmentId::Create(0, 0);
{
// Write an entry containing these two attachment ids.
WriteTransaction trans(FROM_HERE, user_share());
ReadNode root(&trans);
ASSERT_EQ(BaseNode::INIT_OK, root.InitTypeRoot(kType));
WriteNode node(&trans);
node.InitUniqueByCreation(kType, root, "some node");
sync_pb::AttachmentMetadata metadata;
sync_pb::AttachmentMetadataRecord* record1 = metadata.add_record();
*record1->mutable_id() = id1.GetProto();
sync_pb::AttachmentMetadataRecord* record2 = metadata.add_record();
*record2->mutable_id() = id2.GetProto();
record2->set_is_on_server(true);
node.SetAttachmentMetadata(metadata);
}
// Construct the GenericChangeProcessor and see that it asks the
// AttachmentService to upload id1 only.
ConstructGenericChangeProcessor(kType);
ASSERT_EQ(1U, mock_attachment_service()->attachment_id_lists()->size());
ASSERT_THAT(mock_attachment_service()->attachment_id_lists()->front(),
testing::UnorderedElementsAre(id1));
}
// Test that attempting to add an entry that already exists still works.
TEST_F(SyncGenericChangeProcessorTest, AddExistingEntry) {
InitializeForType(SESSIONS);
sync_pb::EntitySpecifics sessions_specifics;
sessions_specifics.mutable_session()->set_session_tag("session tag");
SyncChangeList changes;
// First add it normally.
changes.push_back(
SyncChange(FROM_HERE, SyncChange::ACTION_ADD,
SyncData::CreateLocalData(base::StringPrintf("tag"),
base::StringPrintf("title"),
sessions_specifics)));
ASSERT_FALSE(
change_processor()->ProcessSyncChanges(FROM_HERE, changes).IsSet());
// Now attempt to add it again, but with different specifics. Should not
// result in an error and should still update the specifics.
sessions_specifics.mutable_session()->set_session_tag("session tag 2");
changes[0] = SyncChange(FROM_HERE, SyncChange::ACTION_ADD,
SyncData::CreateLocalData(base::StringPrintf("tag"),
base::StringPrintf("title"),
sessions_specifics));
ASSERT_FALSE(
change_processor()->ProcessSyncChanges(FROM_HERE, changes).IsSet());
// Verify the data was updated properly.
SyncDataList sync_data = change_processor()->GetAllSyncData(SESSIONS);
ASSERT_EQ(sync_data.size(), 1U);
ASSERT_EQ("session tag 2",
sync_data[0].GetSpecifics().session().session_tag());
EXPECT_FALSE(SyncDataRemote(sync_data[0]).GetClientTagHash().empty());
}
} // namespace
} // namespace syncer