blob: 4c9f69548607cf00522e4be4e51febe4f44bfce0 [file] [log] [blame]
// 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/group_data_model.h"
#include <cstdint>
#include <memory>
#include "base/files/scoped_temp_dir.h"
#include "base/files/scoped_temp_file.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "components/data_sharing/internal/collaboration_group_sync_bridge.h"
#include "components/data_sharing/public/features.h"
#include "components/data_sharing/public/group_data.h"
#include "components/data_sharing/test_support/fake_data_sharing_sdk_delegate.h"
#include "components/sync/model/entity_change.h"
#include "components/sync/test/data_type_store_test_util.h"
#include "components/sync/test/mock_data_type_local_change_processor.h"
#include "google_apis/gaia/gaia_id.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace data_sharing {
namespace {
using base::test::RunClosure;
using testing::_;
using testing::ElementsAre;
using testing::Eq;
using testing::IsEmpty;
using testing::Optional;
bool OptionalGroupMetadataDataMatches(std::optional<GroupData> a,
std::optional<GroupData> b) {
if (!a.has_value() && !b.has_value()) {
return true;
}
if (a.has_value() != b.has_value()) {
return false;
}
// Both have values, compare the GroupData.
return a->group_token.group_id == b->group_token.group_id &&
a->group_token.access_token == b->group_token.access_token &&
a->display_name == b->display_name;
}
MATCHER_P(OptionalGroupMetadataDataEq, expected_group_data, "") {
return OptionalGroupMetadataDataMatches(expected_group_data, arg);
}
// TODO(crbug.com/301390275): move helpers to work with CollaborationGroup
// entities to test utils files, they are used across multiple files.
sync_pb::CollaborationGroupSpecifics MakeSpecifics(
const GroupId& id,
const int64_t& changed_at_millis_since_unix_epoch) {
sync_pb::CollaborationGroupSpecifics result;
result.set_collaboration_id(id.value());
result.set_changed_at_timestamp_millis_since_unix_epoch(
changed_at_millis_since_unix_epoch);
result.set_consistency_token(
base::NumberToString(changed_at_millis_since_unix_epoch));
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());
}
MATCHER(NotNullTime, "") {
return !arg.is_null();
}
MATCHER_P(HasDisplayName, expected_name, "") {
return arg.display_name == expected_name;
}
MATCHER_P(HasMemberWithGaiaId, expected_gaia_id, "") {
for (const auto& member : arg.members) {
if (member.gaia_id == expected_gaia_id) {
return true;
}
}
return false;
}
MATCHER_P3(GroupEventIs, group_id, event_type, affected_member_gaia_id, "") {
return arg.group_id == group_id && arg.event_type == event_type &&
arg.affected_member_gaia_id == affected_member_gaia_id &&
!arg.event_time.is_null();
}
class MockModelObserver : public GroupDataModel::Observer {
public:
MockModelObserver() = default;
~MockModelObserver() override = default;
MOCK_METHOD(void, OnModelLoaded, (), (override));
MOCK_METHOD(void,
OnGroupAdded,
(const GroupId& group_id, const base::Time& event_time),
(override));
MOCK_METHOD(void,
OnGroupUpdated,
(const GroupId& group_id, const base::Time& event_time),
(override));
MOCK_METHOD(void,
OnGroupDeleted,
(const GroupId& group_id,
const std::optional<GroupData>& group_data,
const base::Time& event_time),
(override));
MOCK_METHOD(void,
OnMemberAdded,
(const GroupId&, const GaiaId&, const base::Time&),
(override));
MOCK_METHOD(void,
OnMemberRemoved,
(const GroupId&, const GaiaId&, const base::Time&),
(override));
MOCK_METHOD(void, OnSyncBridgeUpdateTypeChanged, (SyncBridgeUpdateType));
};
class GroupDataModelTest : public testing::Test {
public:
GroupDataModelTest()
: data_type_store_(
syncer::DataTypeStoreTestUtil::CreateInMemoryStoreForTest()) {
EXPECT_TRUE(data_sharing_dir_.CreateUniqueTempDir());
}
~GroupDataModelTest() override = default;
void SetUp() override {
base::RunLoop run_loop;
ON_CALL(mock_processor_, ModelReadyToSync)
.WillByDefault(RunClosure(run_loop.QuitClosure()));
collaboration_group_bridge_ =
std::make_unique<CollaborationGroupSyncBridge>(
mock_processor_.CreateForwardingProcessor(),
syncer::DataTypeStoreTestUtil::FactoryForForwardingStore(
data_type_store_.get()));
run_loop.Run();
// Mimic that initial sync is completed with no data.
collaboration_group_bridge_->MergeFullSyncData(
collaboration_group_bridge_->CreateMetadataChangeList(),
syncer::EntityChangeList());
model_ = std::make_unique<GroupDataModel>(data_sharing_dir_.GetPath(),
collaboration_group_bridge_.get(),
&sdk_delegate_);
model_->AddObserver(&observer_);
}
void TearDown() override {
// Needed to ensure that `data_sharing_dir_` outlives DB tasks, that runs on
// a dedicated sequence.
ShutdownModel();
}
GroupDataModel& model() { return *model_; }
testing::NiceMock<MockModelObserver>& model_observer() { return observer_; }
FakeDataSharingSDKDelegate& sdk_delegate() { return sdk_delegate_; }
CollaborationGroupSyncBridge& collaboration_group_bridge() {
return *collaboration_group_bridge_;
}
void WaitForModelLoaded() {
if (model_->IsModelLoaded()) {
return;
}
base::RunLoop run_loop;
EXPECT_CALL(observer_, OnModelLoaded)
.WillOnce(RunClosure(run_loop.QuitClosure()));
run_loop.Run();
}
GroupId MimicGroupAddedServerSide(const std::string& display_name) {
const GroupId id = sdk_delegate_.AddGroupAndReturnId(display_name);
syncer::EntityChangeList entity_changes;
entity_changes.push_back(EntityChangeAddFromSpecifics(
MakeSpecifics(id, next_changed_at_millis_since_unix_epoch_++)));
collaboration_group_bridge_->ApplyIncrementalSyncChanges(
collaboration_group_bridge_->CreateMetadataChangeList(),
std::move(entity_changes));
return id;
}
std::vector<GroupId> MimicMultipleGroupsAddedServerSide(
size_t number_of_groups) {
syncer::EntityChangeList entity_changes;
std::vector<GroupId> group_ids;
for (size_t i = 0; i < number_of_groups; i++) {
std::string display_name = "Group" + base::NumberToString(i);
const GroupId id = sdk_delegate_.AddGroupAndReturnId(display_name);
group_ids.emplace_back(id);
entity_changes.push_back(EntityChangeAddFromSpecifics(
MakeSpecifics(id, next_changed_at_millis_since_unix_epoch_++)));
}
collaboration_group_bridge_->ApplyIncrementalSyncChanges(
collaboration_group_bridge_->CreateMetadataChangeList(),
std::move(entity_changes));
return group_ids;
}
void WaitForGroupAdded(const GroupId& group_id) {
base::RunLoop run_loop;
EXPECT_CALL(observer_, OnGroupAdded(group_id, NotNullTime()))
.WillOnce(RunClosure(run_loop.QuitClosure()));
run_loop.Run();
}
void WaitForMultipleGroupsAdded(size_t number_of_groups) {
base::RunLoop run_loop;
size_t call_count = 0;
EXPECT_CALL(observer_, OnGroupAdded(_, NotNullTime()))
.Times(::testing::AtLeast(0))
.WillRepeatedly(::testing::DoAll(::testing::Invoke([&]() {
++call_count;
if (call_count == number_of_groups) {
run_loop.Quit();
}
})));
run_loop.Run();
}
void MimicMemberAddedServerSide(const GroupId& group_id,
const GaiaId& member_gaia_id) {
sdk_delegate_.AddMember(group_id, member_gaia_id);
syncer::EntityChangeList entity_changes;
entity_changes.push_back(EntityChangeUpdateFromSpecifics(
MakeSpecifics(group_id, next_changed_at_millis_since_unix_epoch_++)));
collaboration_group_bridge_->ApplyIncrementalSyncChanges(
collaboration_group_bridge_->CreateMetadataChangeList(),
std::move(entity_changes));
}
void MimicMemberRemovedServerSide(const GroupId& group_id,
const GaiaId& member_gaia_id) {
sdk_delegate_.RemoveMember(group_id, member_gaia_id);
syncer::EntityChangeList entity_changes;
entity_changes.push_back(EntityChangeUpdateFromSpecifics(
MakeSpecifics(group_id, next_changed_at_millis_since_unix_epoch_++)));
collaboration_group_bridge_->ApplyIncrementalSyncChanges(
collaboration_group_bridge_->CreateMetadataChangeList(),
std::move(entity_changes));
}
void WaitForGroupUpdated(const GroupId& group_id) {
base::RunLoop run_loop;
EXPECT_CALL(observer_, OnGroupUpdated(group_id, NotNullTime()))
.WillOnce(RunClosure(run_loop.QuitClosure()));
run_loop.Run();
}
void MimicGroupDeletedServerSide(const GroupId& group_id) {
sdk_delegate_.RemoveGroup(group_id);
syncer::EntityChangeList entity_changes;
entity_changes.push_back(EntityChangeDeleteFromSpecifics(
MakeSpecifics(group_id, next_changed_at_millis_since_unix_epoch_++)));
collaboration_group_bridge_->ApplyIncrementalSyncChanges(
collaboration_group_bridge_->CreateMetadataChangeList(),
std::move(entity_changes));
}
void WaitForGroupDeleted(const GroupId& group_id) {
// Unlike additions/updates deletions might be handled synchronously, so we
// need to check whether the group was already deleted.
std::optional<GroupData> group_data = model().GetGroup(group_id);
if (!group_data.has_value()) {
return;
}
ASSERT_TRUE(group_data.has_value());
base::RunLoop run_loop;
EXPECT_CALL(
observer_,
OnGroupDeleted(group_id, OptionalGroupMetadataDataEq(group_data),
NotNullTime()))
.WillOnce(RunClosure(run_loop.QuitClosure()));
run_loop.Run();
}
void ShutdownModel() {
base::RunLoop run_loop;
model_->GetGroupDataStoreForTesting().SetShutdownCallbackForTesting(
run_loop.QuitClosure());
model_->RemoveObserver(&observer_);
model_.reset();
// Wait for DB shutdown tasks completion.
run_loop.Run();
}
void RestartModel() {
model_ = std::make_unique<GroupDataModel>(data_sharing_dir_.GetPath(),
collaboration_group_bridge_.get(),
&sdk_delegate_);
model_->AddObserver(&observer_);
}
void FastForwardBy(const base::TimeDelta& time_delta) {
task_environment_.FastForwardBy(time_delta);
}
private:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
base::ScopedTempDir data_sharing_dir_;
std::unique_ptr<syncer::DataTypeStore> data_type_store_;
testing::NiceMock<syncer::MockDataTypeLocalChangeProcessor> mock_processor_;
std::unique_ptr<CollaborationGroupSyncBridge> collaboration_group_bridge_;
FakeDataSharingSDKDelegate sdk_delegate_;
std::unique_ptr<GroupDataModel> model_;
// Used to ensure that changed_at_timestamp_millis_since_unix_epoch is always
// advanced when changes are made (base::Time::Now() doesn't guarantee that in
// some cases).
int64_t next_changed_at_millis_since_unix_epoch_ = 1000;
testing::NiceMock<MockModelObserver> observer_;
};
TEST_F(GroupDataModelTest, ShouldGetGroup) {
WaitForModelLoaded();
EXPECT_FALSE(model().GetGroup(GroupId("non-existing-group-id")).has_value());
const std::string group_display_name = "group";
const GroupId group_id = MimicGroupAddedServerSide(group_display_name);
WaitForGroupAdded(group_id);
EXPECT_THAT(model().GetGroup(group_id),
Optional(HasDisplayName(group_display_name)));
}
TEST_F(GroupDataModelTest, ShouldGetAllGroups) {
WaitForModelLoaded();
EXPECT_TRUE(model().GetAllGroups().empty());
const std::string group_display_name1 = "group1";
const GroupId group_id1 = MimicGroupAddedServerSide(group_display_name1);
WaitForGroupAdded(group_id1);
EXPECT_THAT(model().GetAllGroups(),
ElementsAre(HasDisplayName(group_display_name1)));
const std::string group_display_name2 = "group2";
const GroupId group_id2 = MimicGroupAddedServerSide(group_display_name2);
WaitForGroupAdded(group_id2);
EXPECT_THAT(model().GetAllGroups(),
ElementsAre(HasDisplayName(group_display_name1),
HasDisplayName(group_display_name2)));
}
TEST_F(GroupDataModelTest, FetchWorksCorrectlyForLargeNumberOfGroups) {
WaitForModelLoaded();
EXPECT_TRUE(model().GetAllGroups().empty());
size_t number_of_groups = 250;
const std::vector<GroupId> group_ids =
MimicMultipleGroupsAddedServerSide(number_of_groups);
ASSERT_EQ(number_of_groups, group_ids.size());
WaitForMultipleGroupsAdded(number_of_groups);
EXPECT_EQ(number_of_groups, model().GetAllGroups().size());
for (const GroupId& group_id : group_ids) {
EXPECT_TRUE(model().GetGroup(group_id).has_value());
}
}
TEST_F(GroupDataModelTest, ShouldUpdateGroup) {
WaitForModelLoaded();
const GroupId group_id = MimicGroupAddedServerSide("group");
WaitForGroupAdded(group_id);
const GaiaId member_gaia_id("gaia_id");
MimicMemberAddedServerSide(group_id, member_gaia_id);
WaitForGroupUpdated(group_id);
EXPECT_THAT(model().GetGroup(group_id),
Optional(HasMemberWithGaiaId(member_gaia_id)));
}
TEST_F(GroupDataModelTest, ShouldHandlePartialReadGroupsFailure) {
WaitForModelLoaded();
// Both groups are in CollaborationGroupSyncBridge, but only one is in
// DataSharingSDKDelegate.
const std::string display_name = "group";
const GroupId group_id1 = sdk_delegate().AddGroupAndReturnId(display_name);
const GroupId group_id2 = GroupId("non_existent_group_id");
syncer::EntityChangeList entity_changes;
entity_changes.push_back(EntityChangeAddFromSpecifics(
MakeSpecifics(group_id1, /*changed_at_millis_since_unix_epoch=*/1000)));
entity_changes.push_back(EntityChangeAddFromSpecifics(
MakeSpecifics(group_id2, /*changed_at_millis_since_unix_epoch=*/1000)));
collaboration_group_bridge().ApplyIncrementalSyncChanges(
collaboration_group_bridge().CreateMetadataChangeList(),
std::move(entity_changes));
// First group should be successfully read.
base::RunLoop run_loop;
EXPECT_CALL(model_observer(), OnGroupAdded(group_id1, NotNullTime()))
.WillOnce(RunClosure(run_loop.QuitClosure()));
run_loop.Run();
EXPECT_THAT(model().GetGroup(group_id1),
Optional(HasDisplayName(display_name)));
EXPECT_FALSE(model().GetGroup(group_id2).has_value());
}
TEST_F(GroupDataModelTest, ShouldNotifyAboutGroupChanges) {
WaitForModelLoaded();
const GroupId group_id = MimicGroupAddedServerSide("group");
WaitForGroupAdded(group_id);
// Test that OnMemberAdded() is called when a member is added.
const GaiaId member_gaia_id("gaia_id");
EXPECT_CALL(model_observer(),
OnMemberAdded(group_id, member_gaia_id, NotNullTime()));
MimicMemberAddedServerSide(group_id, member_gaia_id);
WaitForGroupUpdated(group_id);
testing::Mock::VerifyAndClearExpectations(&model_observer());
// Test that OnMemberRemoved() is called when a member is removed.
EXPECT_CALL(model_observer(),
OnMemberRemoved(group_id, member_gaia_id, NotNullTime()));
MimicMemberRemovedServerSide(group_id, member_gaia_id);
WaitForGroupUpdated(group_id);
}
TEST_F(GroupDataModelTest, ShouldNotifyOnSyncBridgeUpdateTypeChanged) {
EXPECT_CALL(model_observer(), OnSyncBridgeUpdateTypeChanged(
Eq(SyncBridgeUpdateType::kDisableSync)))
.Times(1);
model().OnSyncBridgeUpdateTypeChanged(SyncBridgeUpdateType::kDisableSync);
}
TEST_F(GroupDataModelTest, ShouldDeleteGroup) {
WaitForModelLoaded();
const GroupId group_id = MimicGroupAddedServerSide("group");
WaitForGroupAdded(group_id);
std::optional<GroupData> group_data = model().GetGroup(group_id);
ASSERT_TRUE(group_data.has_value());
// Unlike additions/updates deletions are handled synchronously, once
// CollaborationGroupSyncBridge received the update - no need to wait for
// observer call with RunLoop.
EXPECT_CALL(model_observer(),
OnGroupDeleted(group_id, OptionalGroupMetadataDataEq(group_data),
NotNullTime()));
MimicGroupDeletedServerSide(group_id);
EXPECT_FALSE(model().GetGroup(group_id).has_value());
}
TEST_F(GroupDataModelTest, ShouldPersistDataAcrossRestart) {
WaitForModelLoaded();
const std::string group_display_name = "group";
const GroupId group_id = MimicGroupAddedServerSide(group_display_name);
WaitForGroupAdded(group_id);
ASSERT_THAT(model().GetGroup(group_id),
Optional(HasDisplayName(group_display_name)));
ShutdownModel();
RestartModel();
WaitForModelLoaded();
EXPECT_THAT(model().GetGroup(group_id),
Optional(HasDisplayName(group_display_name)));
}
TEST_F(GroupDataModelTest, ShouldHandleNewGroupsAfterRestart) {
WaitForModelLoaded();
ShutdownModel();
// Mimic that new group addition was only partially handled:
// CollaborationGroupSyncBridge is still running and will persist changes, but
// model is shut down so it can't process them.
const std::string group_display_name = "group";
const GroupId group_id = MimicGroupAddedServerSide(group_display_name);
RestartModel();
WaitForModelLoaded();
WaitForGroupAdded(group_id);
EXPECT_THAT(model().GetGroup(group_id),
Optional(HasDisplayName(group_display_name)));
}
TEST_F(GroupDataModelTest, ShouldHandleUpdatesAfterRestart) {
WaitForModelLoaded();
const std::string group_display_name = "group";
const GroupId group_id = MimicGroupAddedServerSide(group_display_name);
WaitForGroupAdded(group_id);
ASSERT_THAT(model().GetGroup(group_id),
Optional(HasDisplayName(group_display_name)));
// Mimic that new group addition was only partially handled:
// CollaborationGroupSyncBridge is still running and will persist changes, but
// model is shut down so it can't process them.
ShutdownModel();
const GaiaId member_gaia_id("gaia_id");
MimicMemberAddedServerSide(group_id, member_gaia_id);
RestartModel();
WaitForModelLoaded();
WaitForGroupUpdated(group_id);
EXPECT_THAT(model().GetGroup(group_id),
Optional(HasMemberWithGaiaId(member_gaia_id)));
}
TEST_F(GroupDataModelTest, ShouldHandleDeletionsAfterRestart) {
WaitForModelLoaded();
const std::string group_display_name = "group";
const GroupId group_id = MimicGroupAddedServerSide(group_display_name);
WaitForGroupAdded(group_id);
ASSERT_THAT(model().GetGroup(group_id),
Optional(HasDisplayName(group_display_name)));
ShutdownModel();
// Mimic that deletion was only partially handled:
// CollaborationGroupSyncBridge is still running and will persist changes, but
// model is shut down so it can't process them.
MimicGroupDeletedServerSide(group_id);
RestartModel();
WaitForModelLoaded();
EXPECT_FALSE(model().GetGroup(group_id).has_value());
}
TEST_F(GroupDataModelTest, ShouldGetPossiblyRemovedGroupMember) {
WaitForModelLoaded();
const GroupId group_id = MimicGroupAddedServerSide("group");
WaitForGroupAdded(group_id);
const GaiaId member_gaia_id("gaia_id");
MimicMemberAddedServerSide(group_id, member_gaia_id);
WaitForGroupUpdated(group_id);
// Existing member should be returned.
const auto member_data_opt =
model().GetPossiblyRemovedGroupMember(group_id, member_gaia_id);
ASSERT_TRUE(member_data_opt.has_value());
EXPECT_EQ(member_data_opt->gaia_id, member_gaia_id);
// Group never existed, nullopt should be returned.
EXPECT_FALSE(model()
.GetPossiblyRemovedGroupMember(GroupId("non-existing-group"),
member_gaia_id)
.has_value());
// Member never existed, nullopt should be returned.
EXPECT_FALSE(model()
.GetPossiblyRemovedGroupMember(group_id,
GaiaId("non-existing-member"))
.has_value());
// TODO(crbug.com/373628741): add coverage for the scenario when member was
// removed from the group once it is properly supported (i.e. removed members
// data is temporarily stored).
}
TEST(GroupDataModelTestNoFixture, ShouldRecordDBInitFailure) {
base::test::TaskEnvironment task_environment;
// Boilerplate to create a bridge / SDK delegate (required to create a model,
// but otherwise not relevant for this test)
std::unique_ptr<syncer::DataTypeStore> data_type_store(
syncer::DataTypeStoreTestUtil::CreateInMemoryStoreForTest());
testing::NiceMock<syncer::MockDataTypeLocalChangeProcessor> mock_processor;
auto collaboration_group_bridge =
std::make_unique<CollaborationGroupSyncBridge>(
mock_processor.CreateForwardingProcessor(),
syncer::DataTypeStoreTestUtil::FactoryForForwardingStore(
data_type_store.get()));
FakeDataSharingSDKDelegate sdk_delegate;
// Model expects a directory, not a file and this will cause DB init failure.
base::ScopedTempFile temp_file;
ASSERT_TRUE(temp_file.Create());
GroupDataModel model(temp_file.path(), collaboration_group_bridge.get(),
&sdk_delegate);
base::HistogramTester histogram_tester;
base::RunLoop run_loop;
model.SetGroupDataStoreLoadedCallbackForTesting(run_loop.QuitClosure());
run_loop.Run();
histogram_tester.ExpectUniqueSample("DataSharing.GroupDBInitSuccess", false,
1);
}
TEST_F(GroupDataModelTest, ShouldRecordDBInitSuccess) {
base::HistogramTester histogram_tester;
WaitForModelLoaded();
histogram_tester.ExpectUniqueSample("DataSharing.GroupDBInitSuccess", true,
1);
}
TEST_F(GroupDataModelTest, ShouldRecordGroupEvents) {
WaitForModelLoaded();
// Verify that group addition is recorded.
const GroupId group_id = MimicGroupAddedServerSide("group1");
WaitForGroupAdded(group_id);
EXPECT_THAT(model().GetGroupEventsSinceStartup(),
ElementsAre(GroupEventIs(
group_id, GroupEvent::EventType::kGroupAdded, std::nullopt)));
// Verify that member addition is recorded.
const GaiaId member_gaia_id("gaia_id");
MimicMemberAddedServerSide(group_id, member_gaia_id);
WaitForGroupUpdated(group_id);
EXPECT_THAT(
model().GetGroupEventsSinceStartup(),
ElementsAre(GroupEventIs(group_id, GroupEvent::EventType::kGroupAdded,
std::nullopt),
GroupEventIs(group_id, GroupEvent::EventType::kMemberAdded,
member_gaia_id)));
// Verify that member removal is recorded.
MimicMemberRemovedServerSide(group_id, member_gaia_id);
WaitForGroupUpdated(group_id);
EXPECT_THAT(
model().GetGroupEventsSinceStartup(),
ElementsAre(GroupEventIs(group_id, GroupEvent::EventType::kGroupAdded,
std::nullopt),
GroupEventIs(group_id, GroupEvent::EventType::kMemberAdded,
member_gaia_id),
GroupEventIs(group_id, GroupEvent::EventType::kMemberRemoved,
member_gaia_id)));
// Verify that group removal is recorded.
MimicGroupDeletedServerSide(group_id);
WaitForGroupDeleted(group_id);
EXPECT_THAT(
model().GetGroupEventsSinceStartup(),
ElementsAre(GroupEventIs(group_id, GroupEvent::EventType::kGroupAdded,
std::nullopt),
GroupEventIs(group_id, GroupEvent::EventType::kMemberAdded,
member_gaia_id),
GroupEventIs(group_id, GroupEvent::EventType::kMemberRemoved,
member_gaia_id),
GroupEventIs(group_id, GroupEvent::EventType::kGroupRemoved,
std::nullopt)));
}
TEST_F(GroupDataModelTest, ShouldDoPeriodicPolling) {
WaitForModelLoaded();
const GroupId group_id = MimicGroupAddedServerSide("group1");
WaitForGroupAdded(group_id);
const GaiaId member_gaia_id("gaia_id");
MimicMemberAddedServerSide(group_id, member_gaia_id);
WaitForGroupUpdated(group_id);
{
std::optional<GroupData> group_data = model().GetGroup(group_id);
ASSERT_TRUE(group_data.has_value());
ASSERT_THAT(*group_data, HasMemberWithGaiaId(member_gaia_id));
}
// Mimic that member was removed from the group without updating
// CollaborationGroup (thus model doesn't know about it).
sdk_delegate().RemoveMember(group_id, member_gaia_id);
FastForwardBy(base::Hours(1));
{
// One hour is not long enough to trigger periodic polling, so the group
// member should still be present.
std::optional<GroupData> group_data = model().GetGroup(group_id);
ASSERT_TRUE(group_data.has_value());
ASSERT_THAT(*group_data, HasMemberWithGaiaId(member_gaia_id));
}
// There will be extra OnGroupUpdated() call after periodic polling, so to
// avoid unexpected call errors, verify and clear expectations.
testing::Mock::VerifyAndClearExpectations(&model_observer());
base::RunLoop run_loop;
EXPECT_CALL(model_observer(),
OnMemberRemoved(group_id, member_gaia_id, NotNullTime()))
.WillOnce(RunClosure(run_loop.QuitClosure()));
// Periodic polling is attempted once per hour and only effective for groups
// that were updated more that kDataSharingGroupDataPeriodicPollingInterval
// ago, so advance time by the sum of both.
FastForwardBy(features::kDataSharingGroupDataPeriodicPollingInterval.Get() +
base::Hours(1));
run_loop.Run();
{
// Now model should be aware of the member removal.
std::optional<GroupData> group_data = model().GetGroup(group_id);
ASSERT_TRUE(group_data.has_value());
EXPECT_TRUE(group_data->members.empty());
}
}
} // namespace
} // namespace data_sharing