// Copyright 2015 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/device_info_service.h" #include #include #include #include #include "base/bind.h" #include "base/memory/scoped_ptr.h" #include "base/message_loop/message_loop.h" #include "base/run_loop.h" #include "base/strings/stringprintf.h" #include "components/sync_driver/local_device_info_provider_mock.h" #include "sync/api/data_batch.h" #include "sync/api/metadata_batch.h" #include "sync/api/model_type_store.h" #include "sync/internal_api/public/test/model_type_store_test_util.h" #include "sync/protocol/data_type_state.pb.h" #include "testing/gtest/include/gtest/gtest.h" namespace sync_driver_v2 { using syncer::SyncError; using syncer_v2::DataBatch; using syncer_v2::EntityChange; using syncer_v2::EntityChangeList; using syncer_v2::EntityData; using syncer_v2::EntityDataPtr; using syncer_v2::MetadataBatch; using syncer_v2::MetadataChangeList; using syncer_v2::ModelTypeChangeProcessor; using syncer_v2::ModelTypeService; using syncer_v2::ModelTypeStore; using syncer_v2::ModelTypeStoreTestUtil; using syncer_v2::TagAndData; using sync_driver::DeviceInfo; using sync_driver::DeviceInfoTracker; using sync_driver::LocalDeviceInfoProviderMock; using sync_pb::DataTypeState; using sync_pb::DeviceInfoSpecifics; using sync_pb::EntitySpecifics; using ClientTagList = ModelTypeService::ClientTagList; using RecordList = ModelTypeStore::RecordList; using Result = ModelTypeStore::Result; using WriteBatch = ModelTypeStore::WriteBatch; namespace { void AssertResultIsSuccess(Result result) { ASSERT_EQ(Result::SUCCESS, result); } void AssertEqual(const DeviceInfoSpecifics& s1, const DeviceInfoSpecifics& s2) { ASSERT_EQ(s1.cache_guid(), s2.cache_guid()); ASSERT_EQ(s1.client_name(), s2.client_name()); ASSERT_EQ(s1.device_type(), s2.device_type()); ASSERT_EQ(s1.sync_user_agent(), s2.sync_user_agent()); ASSERT_EQ(s1.chrome_version(), s2.chrome_version()); ASSERT_EQ(s1.signin_scoped_device_id(), s2.signin_scoped_device_id()); } void AssertEqual(const DeviceInfoSpecifics& specifics, const DeviceInfo& model) { ASSERT_EQ(specifics.cache_guid(), model.guid()); ASSERT_EQ(specifics.client_name(), model.client_name()); ASSERT_EQ(specifics.device_type(), model.device_type()); ASSERT_EQ(specifics.sync_user_agent(), model.sync_user_agent()); ASSERT_EQ(specifics.chrome_version(), model.chrome_version()); ASSERT_EQ(specifics.signin_scoped_device_id(), model.signin_scoped_device_id()); } void AssertErrorFromDataBatch(SyncError error, scoped_ptr batch) { ASSERT_TRUE(error.IsSet()); } void AssertExpectedFromDataBatch( std::map expected, SyncError error, scoped_ptr batch) { ASSERT_FALSE(error.IsSet()); while (batch->HasNext()) { const TagAndData& pair = batch->Next(); std::map::iterator iter = expected.find(pair.first); ASSERT_NE(iter, expected.end()); AssertEqual(iter->second, pair.second->specifics.device_info()); // Removing allows us to verify we don't see the same item multiple times, // and that we saw everything we expected. expected.erase(iter); } ASSERT_TRUE(expected.empty()); } // Instead of actually processing anything, simply accumulates all instructions // in members that can then be accessed. TODO(skym): If this ends up being // useful for other model type unittests it should be moved out to a shared // location. class FakeModelTypeChangeProcessor : public ModelTypeChangeProcessor { public: FakeModelTypeChangeProcessor() {} ~FakeModelTypeChangeProcessor() override {} void Put(const std::string& client_tag, scoped_ptr entity_data, MetadataChangeList* metadata_changes) override { put_map_.insert(std::make_pair(client_tag, std::move(entity_data))); } void Delete(const std::string& client_tag, MetadataChangeList* metadata_changes) override { delete_set_.insert(client_tag); } void OnMetadataLoaded(scoped_ptr batch) override { std::swap(metadata_, batch); } const std::map>& put_map() const { return put_map_; } const std::set& delete_set() const { return delete_set_; } const MetadataBatch* metadata() const { return metadata_.get(); } private: std::map> put_map_; std::set delete_set_; scoped_ptr metadata_; }; } // namespace class DeviceInfoServiceTest : public testing::Test, public DeviceInfoTracker::Observer { protected: ~DeviceInfoServiceTest() override { // Some tests may never initialize the service. if (service_) service_->RemoveObserver(this); // Force all remaining (store) tasks to execute so we don't leak memory. base::RunLoop().RunUntilIdle(); } void OnDeviceInfoChange() override { change_count_++; } protected: DeviceInfoServiceTest() : change_count_(0), store_(ModelTypeStoreTestUtil::CreateInMemoryStoreForTest()), local_device_(new LocalDeviceInfoProviderMock( "guid_1", "client_1", "Chromium 10k", "Chrome 10k", sync_pb::SyncEnums_DeviceType_TYPE_LINUX, "device_id")) {} // Initialized the service based on the current local device and store. Can // only be called once per run, as it passes |store_|. void InitializeService() { ASSERT_TRUE(store_); service_.reset(new DeviceInfoService( local_device_.get(), base::Bind(&ModelTypeStoreTestUtil::MoveStoreToCallback, base::Passed(&store_)))); service_->AddObserver(this); } // Creates the service and runs any outstanding tasks. This will typically // cause all initialization callbacks between the sevice and store to fire. void InitializeAndPump() { InitializeService(); base::RunLoop().RunUntilIdle(); } // Creates a new processor and sets it on the service. We typically need to // pump in this scenario because metadata is going to need to be loading from // the store and given to the processor, which is async. void SetProcessorAndPump() { processor_ = new FakeModelTypeChangeProcessor(); service()->set_change_processor(make_scoped_ptr(processor_)); base::RunLoop().RunUntilIdle(); } // Generates a specifics object with slightly differing values. Will generate // the same values on each run of a test because a simple counter is used to // vary field values. DeviceInfoSpecifics GenerateTestSpecifics() { int label = ++generated_count_; DeviceInfoSpecifics specifics; specifics.set_cache_guid(base::StringPrintf("cache guid %d", label)); specifics.set_client_name(base::StringPrintf("client name %d", label)); specifics.set_device_type(sync_pb::SyncEnums_DeviceType_TYPE_LINUX); specifics.set_sync_user_agent( base::StringPrintf("sync user agent %d", label)); specifics.set_chrome_version( base::StringPrintf("chrome version %d", label)); specifics.set_signin_scoped_device_id( base::StringPrintf("signin scoped device id %d", label)); return specifics; } // Helper method to reduce duplicated code between tests. Wraps the given // specifics object in an EntityData and EntityChange of type ACTION_ADD, and // pushes them onto the given change list. The corresponding client tag the // service determines is returned. Instance method because we need access to // service to generate the tag. std::string PushBackEntityChangeAdd(const DeviceInfoSpecifics& specifics, EntityChangeList* changes) { EntityData data; // These tests do not care about the tag hash, but EntityData and friends // cannot differentiate between the default EntityData object if the hash // is unset, which causes pass/copy operations to no-op and things start to // break, so we throw in a junk value and forget about it. data.client_tag_hash = "junk"; *data.specifics.mutable_device_info() = specifics; const std::string tag = service()->GetClientTag(data); changes->push_back(EntityChange::CreateAdd(tag, data.PassToPtr())); return tag; } // Allows access to the store before that will ultimately be used to // initialize the service. ModelTypeStore* store() { EXPECT_TRUE(store_); return store_.get(); } // Get the number of times the service notifies observers of changes. int change_count() { return change_count_; } // Allows overriding the provider before the service is initialized. void set_local_device(scoped_ptr provider) { ASSERT_FALSE(service_); std::swap(local_device_, provider); } LocalDeviceInfoProviderMock* local_device() { return local_device_.get(); } // Allows access to the service after InitializeService() is called. DeviceInfoService* service() { EXPECT_TRUE(service_); return service_.get(); } FakeModelTypeChangeProcessor* processor() { EXPECT_TRUE(processor_); return processor_; } // Should only be called after the service has been initialized. Will first // recover the service's store, so another can be initialized later, and then // deletes the service. void ShutdownService() { ASSERT_TRUE(service_); std::swap(store_, service_->store_); service_->RemoveObserver(this); service_.reset(); } private: int change_count_; // Although we never use this in this class, the in memory model type store // grabs the current task runner from a static accessor which point at this // message loop. Must be declared/initilized before we call the synchronous // CreateInMemoryStoreForTest. base::MessageLoop message_loop_; // Holds the store while the service is not initialized. scoped_ptr store_; scoped_ptr local_device_; // Not initialized immediately (upon test's constructor). This allows each // test case to modify the dependencies the service will be constructed with. scoped_ptr service_; // A non-owning pointer to the processor given to the service. Will be nullptr // before being given to the service, to make ownership easier. FakeModelTypeChangeProcessor* processor_ = nullptr; // A monotonically increasing label for generated specifics objects with data // that is slightly different from eachother. int generated_count_ = 0; }; namespace { TEST_F(DeviceInfoServiceTest, EmptyDataReconciliation) { InitializeService(); ASSERT_EQ(0u, service()->GetAllDeviceInfo().size()); base::RunLoop().RunUntilIdle(); // TODO(skym): crbug.com/582460: Verify reconciliation has happened. } TEST_F(DeviceInfoServiceTest, LocalProviderSubscription) { set_local_device(make_scoped_ptr(new LocalDeviceInfoProviderMock())); InitializeAndPump(); ASSERT_EQ(0u, service()->GetAllDeviceInfo().size()); local_device()->Initialize(make_scoped_ptr( new DeviceInfo("guid_1", "client_1", "Chromium 10k", "Chrome 10k", sync_pb::SyncEnums_DeviceType_TYPE_LINUX, "device_id"))); // TODO(skym): crbug.com/582460: Verify reconciliation has happened. } TEST_F(DeviceInfoServiceTest, NonEmptyStoreLoad) { // Override the provider so that reconciliation never happens. set_local_device(make_scoped_ptr(new LocalDeviceInfoProviderMock())); scoped_ptr batch = store()->CreateWriteBatch(); DeviceInfoSpecifics specifics(GenerateTestSpecifics()); store()->WriteData(batch.get(), "tag", specifics.SerializeAsString()); store()->CommitWriteBatch(std::move(batch), base::Bind(&AssertResultIsSuccess)); InitializeAndPump(); ScopedVector all_device_info(service()->GetAllDeviceInfo()); ASSERT_EQ(1u, all_device_info.size()); AssertEqual(specifics, *all_device_info[0]); AssertEqual(specifics, *service()->GetDeviceInfo("tag").get()); } TEST_F(DeviceInfoServiceTest, GetClientTagNormal) { InitializeAndPump(); const std::string guid = "abc"; EntitySpecifics entity_specifics; entity_specifics.mutable_device_info()->set_cache_guid(guid); EntityData entity_data; entity_data.specifics = entity_specifics; EXPECT_EQ(guid, service()->GetClientTag(entity_data)); } TEST_F(DeviceInfoServiceTest, GetClientTagEmpty) { InitializeAndPump(); EntitySpecifics entity_specifics; entity_specifics.mutable_device_info(); EntityData entity_data; entity_data.specifics = entity_specifics; EXPECT_EQ("", service()->GetClientTag(entity_data)); } TEST_F(DeviceInfoServiceTest, TestInitStoreThenProc) { scoped_ptr batch = store()->CreateWriteBatch(); DeviceInfoSpecifics specifics(GenerateTestSpecifics()); store()->WriteData(batch.get(), "tag", specifics.SerializeAsString()); DataTypeState state; state.set_encryption_key_name("ekn"); store()->WriteGlobalMetadata(batch.get(), state.SerializeAsString()); store()->CommitWriteBatch(std::move(batch), base::Bind(&AssertResultIsSuccess)); InitializeAndPump(); // Verify that we have data. We do this because we're testing that the service // may sometimes come up after our store init is fully completed. ScopedVector all_device_info(service()->GetAllDeviceInfo()); ASSERT_EQ(1u, all_device_info.size()); AssertEqual(specifics, *all_device_info[0]); AssertEqual(specifics, *service()->GetDeviceInfo("tag").get()); SetProcessorAndPump(); ASSERT_TRUE(processor()->metadata()); ASSERT_EQ(state.encryption_key_name(), processor()->metadata()->GetDataTypeState().encryption_key_name()); } TEST_F(DeviceInfoServiceTest, TestInitProcBeforeStoreFinishes) { scoped_ptr batch = store()->CreateWriteBatch(); DeviceInfoSpecifics specifics(GenerateTestSpecifics()); store()->WriteData(batch.get(), "tag", specifics.SerializeAsString()); DataTypeState state; state.set_encryption_key_name("ekn"); store()->WriteGlobalMetadata(batch.get(), state.SerializeAsString()); store()->CommitWriteBatch(std::move(batch), base::Bind(&AssertResultIsSuccess)); InitializeService(); // Verify we have _NO_ data yet, to verify that we're testing when the // processor is attached and ready before our store init is fully completed. ASSERT_EQ(0u, service()->GetAllDeviceInfo().size()); SetProcessorAndPump(); ASSERT_TRUE(processor()->metadata()); ASSERT_EQ(state.encryption_key_name(), processor()->metadata()->GetDataTypeState().encryption_key_name()); } TEST_F(DeviceInfoServiceTest, GetData) { scoped_ptr batch = store()->CreateWriteBatch(); DeviceInfoSpecifics specifics1(GenerateTestSpecifics()); DeviceInfoSpecifics specifics3(GenerateTestSpecifics()); store()->WriteData(batch.get(), "tag1", specifics1.SerializeAsString()); store()->WriteData(batch.get(), "tag2", GenerateTestSpecifics().SerializeAsString()); store()->WriteData(batch.get(), "tag3", specifics3.SerializeAsString()); store()->CommitWriteBatch(std::move(batch), base::Bind(&AssertResultIsSuccess)); InitializeAndPump(); std::map expected; expected["tag1"] = specifics1; expected["tag3"] = specifics3; ClientTagList client_tags; client_tags.push_back("tag1"); client_tags.push_back("tag3"); service()->GetData(client_tags, base::Bind(&AssertExpectedFromDataBatch, expected)); } TEST_F(DeviceInfoServiceTest, GetDataMissing) { InitializeAndPump(); std::map expected; ClientTagList client_tags; client_tags.push_back("tag1"); service()->GetData(client_tags, base::Bind(&AssertExpectedFromDataBatch, expected)); } TEST_F(DeviceInfoServiceTest, GetDataNotInitialized) { InitializeService(); ClientTagList client_tags; service()->GetData(client_tags, base::Bind(&AssertErrorFromDataBatch)); } TEST_F(DeviceInfoServiceTest, GetAllData) { scoped_ptr batch = store()->CreateWriteBatch(); DeviceInfoSpecifics specifics1(GenerateTestSpecifics()); DeviceInfoSpecifics specifics2(GenerateTestSpecifics()); store()->WriteData(batch.get(), "tag1", specifics1.SerializeAsString()); store()->WriteData(batch.get(), "tag2", specifics2.SerializeAsString()); store()->CommitWriteBatch(std::move(batch), base::Bind(&AssertResultIsSuccess)); InitializeAndPump(); std::map expected; expected["tag1"] = specifics1; expected["tag2"] = specifics2; ClientTagList client_tags; client_tags.push_back("tag1"); client_tags.push_back("tag2"); service()->GetData(client_tags, base::Bind(&AssertExpectedFromDataBatch, expected)); } TEST_F(DeviceInfoServiceTest, GetAllDataNotInitialized) { InitializeService(); service()->GetAllData(base::Bind(&AssertErrorFromDataBatch)); } TEST_F(DeviceInfoServiceTest, ApplySyncChangesBeforeInit) { InitializeService(); const SyncError error = service()->ApplySyncChanges( service()->CreateMetadataChangeList(), EntityChangeList()); EXPECT_TRUE(error.IsSet()); EXPECT_EQ(0, change_count()); } TEST_F(DeviceInfoServiceTest, ApplySyncChangesEmpty) { InitializeAndPump(); const SyncError error = service()->ApplySyncChanges( service()->CreateMetadataChangeList(), EntityChangeList()); EXPECT_FALSE(error.IsSet()); EXPECT_EQ(0, change_count()); } TEST_F(DeviceInfoServiceTest, ApplySyncChangesInMemory) { InitializeAndPump(); DeviceInfoSpecifics specifics = GenerateTestSpecifics(); EntityChangeList add_changes; const std::string tag = PushBackEntityChangeAdd(specifics, &add_changes); SyncError error = service()->ApplySyncChanges( service()->CreateMetadataChangeList(), add_changes); EXPECT_FALSE(error.IsSet()); scoped_ptr info = service()->GetDeviceInfo(tag); ASSERT_TRUE(info); AssertEqual(specifics, *info.get()); EXPECT_EQ(1, change_count()); EntityChangeList delete_changes; delete_changes.push_back(EntityChange::CreateDelete(tag)); error = service()->ApplySyncChanges(service()->CreateMetadataChangeList(), delete_changes); EXPECT_FALSE(error.IsSet()); EXPECT_FALSE(service()->GetDeviceInfo(tag)); EXPECT_EQ(2, change_count()); } TEST_F(DeviceInfoServiceTest, ApplySyncChangesStore) { InitializeAndPump(); DeviceInfoSpecifics specifics = GenerateTestSpecifics(); EntityChangeList data_changes; const std::string tag = PushBackEntityChangeAdd(specifics, &data_changes); DataTypeState state; state.set_encryption_key_name("ekn"); scoped_ptr metadata_changes( service()->CreateMetadataChangeList()); metadata_changes->UpdateDataTypeState(state); const SyncError error = service()->ApplySyncChanges(std::move(metadata_changes), data_changes); EXPECT_FALSE(error.IsSet()); EXPECT_EQ(1, change_count()); // Force write/commit tasks to finish before shutdown. base::RunLoop().RunUntilIdle(); ShutdownService(); InitializeAndPump(); SetProcessorAndPump(); scoped_ptr info = service()->GetDeviceInfo(tag); ASSERT_TRUE(info); AssertEqual(specifics, *info.get()); EXPECT_TRUE(processor()->metadata()); // TODO(skym): Uncomment once SimpleMetadataChangeList::TransferChanges is // implemented. // EXPECT_EQ(state.encryption_key_name(), // processor()->metadata()->GetDataTypeState().encryption_key_name()); } TEST_F(DeviceInfoServiceTest, ApplySyncChangesWithLocalGuid) { InitializeAndPump(); // The point of this test is to try to apply remote changes that have the same // cache guid as the local device. The service should ignore these changes // since only it should be performing writes on its data. DeviceInfoSpecifics specifics = GenerateTestSpecifics(); specifics.set_cache_guid(local_device()->GetLocalDeviceInfo()->guid()); EntityChangeList data_changes; const std::string tag = PushBackEntityChangeAdd(specifics, &data_changes); const SyncError error = service()->ApplySyncChanges( service()->CreateMetadataChangeList(), data_changes); EXPECT_FALSE(error.IsSet()); EXPECT_FALSE(service()->GetDeviceInfo(tag)); EXPECT_EQ(0, change_count()); } TEST_F(DeviceInfoServiceTest, ApplyDeleteNonexistent) { InitializeAndPump(); EntityChangeList delete_changes; delete_changes.push_back(EntityChange::CreateDelete("tag")); const SyncError error = service()->ApplySyncChanges( service()->CreateMetadataChangeList(), delete_changes); EXPECT_FALSE(error.IsSet()); EXPECT_EQ(0, change_count()); } } // namespace } // namespace sync_driver_v2