blob: d0d65fcd8ab74a8a7829a966e08b7b1565d7f976 [file]
// 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