| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/data_sharing/internal/collaboration_group_sync_bridge.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <set> |
| |
| #include "base/run_loop.h" |
| #include "base/test/bind.h" |
| #include "base/test/mock_callback.h" |
| #include "base/test/protobuf_matchers.h" |
| #include "base/test/task_environment.h" |
| #include "components/sync/model/data_batch.h" |
| #include "components/sync/model/data_type_store.h" |
| #include "components/sync/protocol/collaboration_group_specifics.pb.h" |
| #include "components/sync/test/data_type_store_test_util.h" |
| #include "components/sync/test/mock_data_type_local_change_processor.h" |
| #include "components/sync/test/test_matchers.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace data_sharing { |
| namespace { |
| |
| using base::test::EqualsProto; |
| using syncer::HasInitialSyncDone; |
| using syncer::IsEmptyMetadataBatch; |
| using syncer::NoModelError; |
| using testing::_; |
| using testing::ElementsAre; |
| using testing::Eq; |
| using testing::InvokeWithoutArgs; |
| using testing::IsEmpty; |
| using testing::IsNull; |
| using testing::Sequence; |
| using testing::SizeIs; |
| using testing::UnorderedElementsAre; |
| |
| sync_pb::CollaborationGroupSpecifics MakeSpecifics( |
| const GroupId& id, |
| const base::Time& changed_at = base::Time::Now()) { |
| sync_pb::CollaborationGroupSpecifics result; |
| result.set_collaboration_id(id.value()); |
| result.set_changed_at_timestamp_millis_since_unix_epoch( |
| changed_at.InMillisecondsSinceUnixEpoch()); |
| return result; |
| } |
| |
| syncer::EntityData EntityDataFromSpecifics( |
| const sync_pb::CollaborationGroupSpecifics& specifics) { |
| syncer::EntityData entity_data; |
| *entity_data.specifics.mutable_collaboration_group() = specifics; |
| entity_data.name = specifics.collaboration_id(); |
| return entity_data; |
| } |
| |
| std::unique_ptr<syncer::EntityChange> EntityChangeAddFromSpecifics( |
| const sync_pb::CollaborationGroupSpecifics& specifics) { |
| return syncer::EntityChange::CreateAdd(specifics.collaboration_id(), |
| EntityDataFromSpecifics(specifics)); |
| } |
| |
| std::unique_ptr<syncer::EntityChange> EntityChangeUpdateFromSpecifics( |
| const sync_pb::CollaborationGroupSpecifics& specifics) { |
| return syncer::EntityChange::CreateUpdate(specifics.collaboration_id(), |
| EntityDataFromSpecifics(specifics)); |
| } |
| |
| std::unique_ptr<syncer::EntityChange> EntityChangeDeleteFromSpecifics( |
| const sync_pb::CollaborationGroupSpecifics& specifics) { |
| return syncer::EntityChange::CreateDelete(specifics.collaboration_id(), |
| syncer::EntityData()); |
| } |
| |
| std::vector<sync_pb::CollaborationGroupSpecifics> ExtractSpecificsFromDataBatch( |
| std::unique_ptr<syncer::DataBatch> data_batch) { |
| std::vector<sync_pb::CollaborationGroupSpecifics> result; |
| while (data_batch->HasNext()) { |
| result.push_back( |
| data_batch->Next().second->specifics.collaboration_group()); |
| } |
| return result; |
| } |
| |
| class MockObserver : public CollaborationGroupSyncBridge::Observer { |
| public: |
| MockObserver() = default; |
| ~MockObserver() override = default; |
| |
| MOCK_METHOD(void, |
| OnGroupsUpdated, |
| (const std::vector<GroupId>&, |
| const std::vector<GroupId>&, |
| const std::vector<GroupId>&), |
| (override)); |
| MOCK_METHOD(void, OnCollaborationGroupSyncDataLoaded, (), (override)); |
| MOCK_METHOD(void, OnSyncBridgeUpdateTypeChanged, (SyncBridgeUpdateType)); |
| }; |
| |
| class CollaborationGroupSyncBridgeTest : public testing::Test { |
| public: |
| CollaborationGroupSyncBridgeTest() |
| : data_type_store_( |
| syncer::DataTypeStoreTestUtil::CreateInMemoryStoreForTest()) {} |
| ~CollaborationGroupSyncBridgeTest() override = default; |
| |
| void TearDown() override { bridge_->RemoveObserver(&observer_); } |
| |
| void CreateBridgeAndWaitForReadyToSync() { |
| base::RunLoop run_loop; |
| ON_CALL(mock_processor_, ModelReadyToSync) |
| .WillByDefault(InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit)); |
| |
| bridge_ = std::make_unique<CollaborationGroupSyncBridge>( |
| mock_processor_.CreateForwardingProcessor(), |
| syncer::DataTypeStoreTestUtil::FactoryForForwardingStore( |
| data_type_store_.get())); |
| |
| bridge_->AddObserver(&observer_); |
| |
| // Wait for ready to sync. |
| run_loop.Run(); |
| } |
| |
| void RestartBridgeAndWaitForReadyToSync() { |
| bridge_->RemoveObserver(&observer_); |
| CreateBridgeAndWaitForReadyToSync(); |
| } |
| |
| CollaborationGroupSyncBridge& bridge() { return *bridge_; } |
| |
| testing::NiceMock<syncer::MockDataTypeLocalChangeProcessor>& |
| mock_processor() { |
| return mock_processor_; |
| } |
| |
| testing::NiceMock<MockObserver>& observer() { return observer_; } |
| |
| syncer::DataTypeStore& data_type_store() { return *data_type_store_; } |
| |
| std::vector<sync_pb::CollaborationGroupSpecifics> GetBridgeSpecifics() { |
| return ExtractSpecificsFromDataBatch(bridge().GetAllDataForDebugging()); |
| } |
| |
| private: |
| base::test::SingleThreadTaskEnvironment task_environment_; |
| |
| std::unique_ptr<syncer::DataTypeStore> data_type_store_; |
| testing::NiceMock<syncer::MockDataTypeLocalChangeProcessor> mock_processor_; |
| testing::NiceMock<MockObserver> observer_; |
| std::unique_ptr<CollaborationGroupSyncBridge> bridge_; |
| }; |
| |
| TEST_F(CollaborationGroupSyncBridgeTest, ShouldReturnClientTag) { |
| CreateBridgeAndWaitForReadyToSync(); |
| EXPECT_TRUE(bridge().SupportsGetClientTag()); |
| const GroupId id("collaboration1"); |
| EXPECT_THAT(bridge().GetClientTag(EntityDataFromSpecifics(MakeSpecifics(id))), |
| Eq(id.value())); |
| } |
| |
| TEST_F(CollaborationGroupSyncBridgeTest, ShouldReturnStorageKey) { |
| CreateBridgeAndWaitForReadyToSync(); |
| EXPECT_TRUE(bridge().SupportsGetStorageKey()); |
| const GroupId id("collaboration1"); |
| EXPECT_THAT( |
| bridge().GetStorageKey(EntityDataFromSpecifics(MakeSpecifics(id))), |
| Eq(id.value())); |
| } |
| |
| TEST_F(CollaborationGroupSyncBridgeTest, ShouldValidateEntityData) { |
| CreateBridgeAndWaitForReadyToSync(); |
| EXPECT_TRUE(bridge().IsEntityDataValid( |
| EntityDataFromSpecifics(MakeSpecifics(GroupId("id"))))); |
| // Specifics without `collaboration_id` considered invalid. |
| EXPECT_FALSE(bridge().IsEntityDataValid( |
| EntityDataFromSpecifics(sync_pb::CollaborationGroupSpecifics()))); |
| } |
| |
| TEST_F(CollaborationGroupSyncBridgeTest, ShouldMergeFullSyncData) { |
| CreateBridgeAndWaitForReadyToSync(); |
| |
| const GroupId id1("id1"); |
| const GroupId id2("id2"); |
| |
| const sync_pb::CollaborationGroupSpecifics specifics1 = |
| MakeSpecifics(id1, base::Time::FromMillisecondsSinceUnixEpoch(1000)); |
| const sync_pb::CollaborationGroupSpecifics specifics2 = |
| MakeSpecifics(id2, base::Time::FromMillisecondsSinceUnixEpoch(2000)); |
| |
| syncer::EntityChangeList entity_changes; |
| entity_changes.push_back(EntityChangeAddFromSpecifics(specifics1)); |
| entity_changes.push_back(EntityChangeAddFromSpecifics(specifics2)); |
| |
| Sequence s; |
| EXPECT_CALL(observer(), OnSyncBridgeUpdateTypeChanged( |
| Eq(SyncBridgeUpdateType::kInitialMerge))) |
| .InSequence(s); |
| EXPECT_CALL(observer(), OnSyncBridgeUpdateTypeChanged( |
| Eq(SyncBridgeUpdateType::kDefaultState))) |
| .InSequence(s); |
| |
| // Mimics initial sync with two entities described above. |
| bridge().MergeFullSyncData(bridge().CreateMetadataChangeList(), |
| std::move(entity_changes)); |
| |
| // Verify that bridge stores these two entities and nothing else. |
| EXPECT_THAT( |
| GetBridgeSpecifics(), |
| UnorderedElementsAre(EqualsProto(specifics1), EqualsProto(specifics2))); |
| EXPECT_THAT(bridge().GetCollaborationGroupIds(), |
| UnorderedElementsAre(id1, id2)); |
| } |
| |
| TEST_F(CollaborationGroupSyncBridgeTest, |
| ShouldApplyIncrementalSyncChangesAndNotifyObserver) { |
| EXPECT_CALL(observer(), OnCollaborationGroupSyncDataLoaded); |
| CreateBridgeAndWaitForReadyToSync(); |
| testing::Mock::VerifyAndClearExpectations(&observer()); |
| |
| const GroupId id1("id1"); |
| const GroupId id2("id2"); |
| |
| const sync_pb::CollaborationGroupSpecifics specifics1 = |
| MakeSpecifics(id1, base::Time::FromMillisecondsSinceUnixEpoch(1000)); |
| sync_pb::CollaborationGroupSpecifics specifics2 = |
| MakeSpecifics(id2, base::Time::FromMillisecondsSinceUnixEpoch(2000)); |
| |
| syncer::EntityChangeList intitial_entity_changes; |
| intitial_entity_changes.push_back(EntityChangeAddFromSpecifics(specifics1)); |
| intitial_entity_changes.push_back(EntityChangeAddFromSpecifics(specifics2)); |
| |
| // Mimics initial sync with two entities described above. |
| EXPECT_CALL(observer(), |
| OnGroupsUpdated( |
| /*added_group_ids*/ UnorderedElementsAre(id1, id2), _, _)); |
| bridge().MergeFullSyncData(bridge().CreateMetadataChangeList(), |
| std::move(intitial_entity_changes)); |
| testing::Mock::VerifyAndClearExpectations(&observer()); |
| |
| // Verify that bridge stores these two entities and nothing else. |
| EXPECT_THAT( |
| GetBridgeSpecifics(), |
| UnorderedElementsAre(EqualsProto(specifics1), EqualsProto(specifics2))); |
| |
| // Mimic incremental update: `specifics1` is deleted, `specifics2` is updated, |
| // `specifics3` is added. |
| const GroupId id3("id3"); |
| const sync_pb::CollaborationGroupSpecifics specifics3 = |
| MakeSpecifics(id3, base::Time::FromMillisecondsSinceUnixEpoch(3000)); |
| specifics2.set_changed_at_timestamp_millis_since_unix_epoch(4000); |
| |
| syncer::EntityChangeList incremental_changes; |
| incremental_changes.push_back(EntityChangeDeleteFromSpecifics(specifics1)); |
| incremental_changes.push_back(EntityChangeUpdateFromSpecifics(specifics2)); |
| incremental_changes.push_back(EntityChangeAddFromSpecifics(specifics3)); |
| |
| EXPECT_CALL(observer(), |
| OnGroupsUpdated(/*added_group_ids*/ ElementsAre(id3), |
| /*updated_group_ids*/ ElementsAre(id2), |
| /*deleted_group_ids*/ ElementsAre(id1))); |
| bridge().ApplyIncrementalSyncChanges(bridge().CreateMetadataChangeList(), |
| std::move(incremental_changes)); |
| |
| // Verify that bridge stores`specifics3` and updated `specifics2`. |
| EXPECT_THAT( |
| GetBridgeSpecifics(), |
| UnorderedElementsAre(EqualsProto(specifics2), EqualsProto(specifics3))); |
| EXPECT_THAT(bridge().GetCollaborationGroupIds(), |
| UnorderedElementsAre(id2, id3)); |
| } |
| |
| TEST_F(CollaborationGroupSyncBridgeTest, ShouldStoreAndLoadMetadata) { |
| // The first call is expected in the very beginning when there is no metadata |
| // yet. Use InSequence to guarantee the sequence of the expected calls. |
| testing::InSequence sequence; |
| EXPECT_CALL(mock_processor(), ModelReadyToSync(IsEmptyMetadataBatch())); |
| |
| CreateBridgeAndWaitForReadyToSync(); |
| |
| // Simulate the initial sync merge. |
| std::unique_ptr<syncer::MetadataChangeList> metadata_changes = |
| bridge().CreateMetadataChangeList(); |
| sync_pb::DataTypeState data_type_state; |
| data_type_state.set_initial_sync_state( |
| sync_pb::DataTypeState::INITIAL_SYNC_DONE); |
| metadata_changes->UpdateDataTypeState(data_type_state); |
| bridge().MergeFullSyncData(std::move(metadata_changes), |
| syncer::EntityChangeList()); |
| |
| // The following call should happen on the second initialization, and it |
| // should have non-empty metadata. |
| EXPECT_CALL(mock_processor(), |
| ModelReadyToSync(MetadataBatchContains(HasInitialSyncDone(), |
| /*entities*/ IsEmpty()))); |
| // Simulate restarting the sync bridge. |
| CreateBridgeAndWaitForReadyToSync(); |
| } |
| |
| TEST_F(CollaborationGroupSyncBridgeTest, ShouldStoreAndLoadData) { |
| CreateBridgeAndWaitForReadyToSync(); |
| |
| const GroupId id1("id1"); |
| const GroupId id2("id2"); |
| |
| const sync_pb::CollaborationGroupSpecifics specifics1 = |
| MakeSpecifics(id1, base::Time::FromMillisecondsSinceUnixEpoch(1000)); |
| sync_pb::CollaborationGroupSpecifics specifics2 = |
| MakeSpecifics(id2, base::Time::FromMillisecondsSinceUnixEpoch(2000)); |
| |
| syncer::EntityChangeList intitial_entity_changes; |
| intitial_entity_changes.push_back(EntityChangeAddFromSpecifics(specifics1)); |
| intitial_entity_changes.push_back(EntityChangeAddFromSpecifics(specifics2)); |
| |
| // Mimics initial sync with two entities described above. |
| bridge().MergeFullSyncData(bridge().CreateMetadataChangeList(), |
| std::move(intitial_entity_changes)); |
| |
| // Simulate restarting the sync bridge. |
| RestartBridgeAndWaitForReadyToSync(); |
| |
| // Verify that bridge still stores these two entities. |
| EXPECT_THAT( |
| GetBridgeSpecifics(), |
| UnorderedElementsAre(EqualsProto(specifics1), EqualsProto(specifics2))); |
| EXPECT_THAT(bridge().GetCollaborationGroupIds(), |
| UnorderedElementsAre(id1, id2)); |
| } |
| |
| TEST_F(CollaborationGroupSyncBridgeTest, ShouldApplyDisableSyncChanges) { |
| CreateBridgeAndWaitForReadyToSync(); |
| |
| const GroupId id1("id1"); |
| const GroupId id2("id2"); |
| |
| // Mimics initial sync with some entities and metadata. |
| std::unique_ptr<syncer::MetadataChangeList> metadata_changes = |
| bridge().CreateMetadataChangeList(); |
| sync_pb::DataTypeState data_type_state; |
| data_type_state.set_initial_sync_state( |
| sync_pb::DataTypeState::INITIAL_SYNC_DONE); |
| metadata_changes->UpdateDataTypeState(data_type_state); |
| |
| syncer::EntityChangeList intitial_entity_changes; |
| intitial_entity_changes.push_back( |
| EntityChangeAddFromSpecifics(MakeSpecifics(id1))); |
| intitial_entity_changes.push_back( |
| EntityChangeAddFromSpecifics(MakeSpecifics(id2))); |
| |
| // Mimics initial sync with some entities and metadata. |
| bridge().MergeFullSyncData(std::move(metadata_changes), |
| std::move(intitial_entity_changes)); |
| |
| testing::Mock::VerifyAndClearExpectations(&observer()); |
| EXPECT_CALL( |
| observer(), |
| OnGroupsUpdated(/*added_group_ids*/ IsEmpty(), |
| /*updated_group_ids*/ IsEmpty(), |
| /*deleted_group_ids*/ UnorderedElementsAre(id1, id2))); |
| Sequence s; |
| EXPECT_CALL(observer(), OnSyncBridgeUpdateTypeChanged( |
| Eq(SyncBridgeUpdateType::kDisableSync))) |
| .InSequence(s); |
| EXPECT_CALL(observer(), OnSyncBridgeUpdateTypeChanged( |
| Eq(SyncBridgeUpdateType::kDefaultState))) |
| .InSequence(s); |
| |
| // Should clear all data and metadata, `delete_metadata_change_list` is not |
| // relevant for this implementation, so nullptr is okay. |
| bridge().ApplyDisableSyncChanges(/*delete_metadata_change_list=*/nullptr); |
| |
| // Verify that bridge doesn't store specifics. |
| EXPECT_THAT(GetBridgeSpecifics(), IsEmpty()); |
| |
| // Verify that data and metadata was removed from disk as well. |
| { |
| base::RunLoop run_loop; |
| base::MockOnceCallback<syncer::DataTypeStore::ReadAllDataCallback::RunType> |
| get_all_data_callback; |
| EXPECT_CALL(get_all_data_callback, |
| Run(NoModelError(), /*data_records*/ Pointee(IsEmpty()))) |
| .WillOnce(InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit)); |
| data_type_store().ReadAllData(get_all_data_callback.Get()); |
| run_loop.Run(); |
| } |
| |
| // Verify that metadata was removed from disk. |
| { |
| base::RunLoop run_loop; |
| base::MockOnceCallback<syncer::DataTypeStore::ReadMetadataCallback::RunType> |
| get_metadata_callback; |
| EXPECT_CALL(get_metadata_callback, |
| Run(NoModelError(), IsEmptyMetadataBatch())) |
| .WillOnce(InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit)); |
| data_type_store().ReadAllMetadata(get_metadata_callback.Get()); |
| run_loop.Run(); |
| } |
| } |
| |
| TEST(CollaborationGroupSyncBridgeNoFixtureTest, ShouldReportIsDataLoaded) { |
| base::test::SingleThreadTaskEnvironment task_environment; |
| |
| std::unique_ptr<syncer::DataTypeStore> data_type_store = |
| syncer::DataTypeStoreTestUtil::CreateInMemoryStoreForTest(); |
| testing::NiceMock<syncer::MockDataTypeLocalChangeProcessor> mock_processor; |
| testing::NiceMock<MockObserver> observer; |
| |
| CollaborationGroupSyncBridge bridge( |
| mock_processor.CreateForwardingProcessor(), |
| syncer::DataTypeStoreTestUtil::FactoryForForwardingStore( |
| data_type_store.get())); |
| bridge.AddObserver(&observer); |
| |
| base::RunLoop run_loop; |
| ON_CALL(mock_processor, ModelReadyToSync) |
| .WillByDefault(InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit)); |
| |
| // Loading is asynchronous, so data loading shouldn't happen yet. |
| EXPECT_FALSE(bridge.IsDataLoaded()); |
| |
| // Data should be loaded by the time ModelReadyToSync() is called. |
| EXPECT_CALL(observer, OnCollaborationGroupSyncDataLoaded); |
| run_loop.Run(); |
| EXPECT_TRUE(bridge.IsDataLoaded()); |
| } |
| |
| TEST_F(CollaborationGroupSyncBridgeTest, RemoveGroupLocally) { |
| EXPECT_CALL(observer(), OnCollaborationGroupSyncDataLoaded); |
| CreateBridgeAndWaitForReadyToSync(); |
| testing::Mock::VerifyAndClearExpectations(&observer()); |
| |
| const GroupId id1("id1"); |
| const GroupId id2("id2"); |
| |
| const sync_pb::CollaborationGroupSpecifics specifics1 = |
| MakeSpecifics(id1, base::Time::FromMillisecondsSinceUnixEpoch(1000)); |
| sync_pb::CollaborationGroupSpecifics specifics2 = |
| MakeSpecifics(id2, base::Time::FromMillisecondsSinceUnixEpoch(2000)); |
| |
| syncer::EntityChangeList intitial_entity_changes; |
| intitial_entity_changes.push_back(EntityChangeAddFromSpecifics(specifics1)); |
| intitial_entity_changes.push_back(EntityChangeAddFromSpecifics(specifics2)); |
| |
| // Mimics initial sync with two entities described above. |
| EXPECT_CALL(observer(), |
| OnGroupsUpdated( |
| /*added_group_ids*/ UnorderedElementsAre(id1, id2), _, _)); |
| bridge().MergeFullSyncData(bridge().CreateMetadataChangeList(), |
| std::move(intitial_entity_changes)); |
| testing::Mock::VerifyAndClearExpectations(&observer()); |
| |
| // Verify that bridge stores these two entities and nothing else. |
| EXPECT_THAT( |
| GetBridgeSpecifics(), |
| UnorderedElementsAre(EqualsProto(specifics1), EqualsProto(specifics2))); |
| |
| EXPECT_CALL(observer(), |
| OnGroupsUpdated(/*added_group_ids*/ IsEmpty(), |
| /*updated_group_ids*/ IsEmpty(), |
| /*deleted_group_ids*/ ElementsAre(id2))); |
| bridge().RemoveGroupLocally(id2); |
| |
| // Verify that bridge stores`specifics3` and updated `specifics2`. |
| EXPECT_THAT(GetBridgeSpecifics(), ElementsAre(EqualsProto(specifics1))); |
| EXPECT_THAT(bridge().GetCollaborationGroupIds(), ElementsAre(id1)); |
| } |
| |
| } // namespace |
| } // namespace data_sharing |