blob: fd471a5213fafadd85e9ed77779819d39e198fbf [file] [log] [blame]
// Copyright 2023 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/data_sharing_service_impl.h"
#include <memory>
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
#include "base/version_info/channel.h"
#include "components/data_sharing/internal/fake_data_sharing_sdk_delegate.h"
#include "components/data_sharing/public/data_sharing_sdk_delegate.h"
#include "components/data_sharing/public/data_sharing_service.h"
#include "components/data_sharing/public/data_sharing_ui_delegate.h"
#include "components/data_sharing/public/features.h"
#include "components/data_sharing/public/protocol/data_sharing_sdk.pb.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "components/sync/model/entity_change.h"
#include "components/sync/model/metadata_change_list.h"
#include "components/sync/protocol/collaboration_group_specifics.pb.h"
#include "components/sync/test/data_type_store_test_util.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/status/status.h"
namespace data_sharing {
namespace {
using base::test::RunClosure;
using testing::Eq;
const char kGroupId[] = "/?-group_id";
const char kEncodedGroupId[] = "%2F%3F-group_id";
const char kTokenBlob[] = "/?-_token";
const char kEncodedTokenBlob[] = "%2F%3F-_token";
sync_pb::CollaborationGroupSpecifics MakeCollaborationGroupSpecifics(
const GroupId& id) {
sync_pb::CollaborationGroupSpecifics result;
result.set_collaboration_id(id.value());
result.set_changed_at_timestamp_millis_since_unix_epoch(
base::Time::Now().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());
}
MATCHER_P(HasDisplayName, expected_name, "") {
return arg.display_name == expected_name;
}
class MockObserver : public DataSharingService::Observer {
public:
MockObserver() = default;
~MockObserver() override = default;
MOCK_METHOD(void, OnGroupChanged, (const GroupData&), (override));
MOCK_METHOD(void, OnGroupAdded, (const GroupData&), (override));
MOCK_METHOD(void, OnGroupRemoved, (const GroupId&), (override));
};
} // namespace
class DataSharingServiceImplTest : public testing::Test {
public:
DataSharingServiceImplTest() = default;
~DataSharingServiceImplTest() override = default;
void SetUp() override {
scoped_refptr<network::SharedURLLoaderFactory> test_url_loader_factory =
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory_);
std::unique_ptr<FakeDataSharingSDKDelegate> sdk_delegate =
std::make_unique<FakeDataSharingSDKDelegate>();
not_owned_sdk_delegate_ = sdk_delegate.get();
data_sharing_service_ = std::make_unique<DataSharingServiceImpl>(
std::move(test_url_loader_factory),
identity_test_env_.identity_manager(),
syncer::DataTypeStoreTestUtil::FactoryForInMemoryStoreForTest(),
version_info::Channel::UNKNOWN, std::move(sdk_delegate),
/*ui_delegate=*/nullptr);
}
protected:
base::test::TaskEnvironment task_environment_;
signin::IdentityTestEnvironment identity_test_env_;
network::TestURLLoaderFactory test_url_loader_factory_;
std::unique_ptr<DataSharingServiceImpl> data_sharing_service_;
raw_ptr<FakeDataSharingSDKDelegate> not_owned_sdk_delegate_;
};
TEST_F(DataSharingServiceImplTest, ConstructionAndEmptyServiceCheck) {
EXPECT_FALSE(data_sharing_service_->IsEmptyService());
}
TEST_F(DataSharingServiceImplTest, GetDataSharingNetworkLoader) {
EXPECT_TRUE(data_sharing_service_->GetDataSharingNetworkLoader());
}
TEST_F(DataSharingServiceImplTest, ShouldCreateGroup) {
// TODO(crbug.com/301390275): add a version of this test for unhappy path.
const std::string display_name = "display_name";
DataSharingService::GroupDataOrFailureOutcome outcome;
base::RunLoop run_loop;
data_sharing_service_->CreateGroup(
display_name,
base::BindLambdaForTesting(
[&run_loop, &outcome](
const DataSharingService::GroupDataOrFailureOutcome& result) {
outcome = result;
run_loop.Quit();
}));
run_loop.Run();
ASSERT_TRUE(outcome.has_value());
EXPECT_THAT(outcome->display_name, Eq(display_name));
ASSERT_TRUE(not_owned_sdk_delegate_->GetGroup(outcome->group_token.group_id)
.has_value());
EXPECT_THAT(not_owned_sdk_delegate_->GetGroup(outcome->group_token.group_id)
->display_name(),
Eq(display_name));
}
TEST_F(DataSharingServiceImplTest, ShouldDeleteGroup) {
// TODO(crbug.com/301390275): add a version of this test for unhappy path.
const GroupId group_id =
not_owned_sdk_delegate_->AddGroupAndReturnId("display_name");
ASSERT_TRUE(not_owned_sdk_delegate_->GetGroup(group_id).has_value());
base::RunLoop run_loop;
base::MockOnceCallback<void(DataSharingService::PeopleGroupActionOutcome)>
callback;
EXPECT_CALL(callback,
Run(DataSharingService::PeopleGroupActionOutcome::kSuccess))
.WillOnce(RunClosure(run_loop.QuitClosure()));
data_sharing_service_->DeleteGroup(group_id, callback.Get());
run_loop.Run();
EXPECT_FALSE(not_owned_sdk_delegate_->GetGroup(group_id).has_value());
}
TEST_F(DataSharingServiceImplTest, ShouldReadGroup) {
// TODO(crbug.com/301390275): add a version of this test for unhappy path.
const std::string display_name = "display_name";
const GroupId group_id =
not_owned_sdk_delegate_->AddGroupAndReturnId(display_name);
DataSharingService::GroupDataOrFailureOutcome outcome;
base::RunLoop run_loop;
data_sharing_service_->ReadGroup(
group_id,
base::BindLambdaForTesting(
[&run_loop, &outcome](
const DataSharingService::GroupDataOrFailureOutcome& result) {
outcome = result;
run_loop.Quit();
}));
run_loop.Run();
ASSERT_TRUE(outcome.has_value());
EXPECT_THAT(outcome->display_name, Eq(display_name));
EXPECT_THAT(outcome->group_token.group_id, Eq(group_id));
}
TEST_F(DataSharingServiceImplTest, ShouldReadAllGroups) {
// TODO(crbug.com/301390275): add a version of this test for unhappy path.
// Delegate stores 2 groups.
const std::string display_name1 = "group1";
const GroupId group_id1 =
not_owned_sdk_delegate_->AddGroupAndReturnId(display_name1);
const std::string display_name2 = "group2";
const GroupId group_id2 =
not_owned_sdk_delegate_->AddGroupAndReturnId(display_name2);
// Mimics initial sync for collaboration group datatype with the same two
// groups.
auto* collaboration_group_bridge =
data_sharing_service_->GetCollaborationGroupSyncBridgeForTesting();
syncer::EntityChangeList entity_changes;
entity_changes.push_back(
EntityChangeAddFromSpecifics(MakeCollaborationGroupSpecifics(group_id1)));
entity_changes.push_back(
EntityChangeAddFromSpecifics(MakeCollaborationGroupSpecifics(group_id2)));
collaboration_group_bridge->MergeFullSyncData(
collaboration_group_bridge->CreateMetadataChangeList(),
std::move(entity_changes));
// Verify that DataSharingService reads the same groups.
DataSharingService::GroupsDataSetOrFailureOutcome outcome;
base::RunLoop run_loop;
data_sharing_service_->ReadAllGroups(base::BindLambdaForTesting(
[&run_loop, &outcome](
const DataSharingService::GroupsDataSetOrFailureOutcome& result) {
outcome = result;
run_loop.Quit();
}));
run_loop.Run();
ASSERT_TRUE(outcome.has_value());
EXPECT_THAT(outcome->size(), Eq(2));
const GroupData& group1 = *outcome->begin();
const GroupData& group2 = *(++outcome->begin());
EXPECT_THAT(group1.display_name, Eq(display_name1));
EXPECT_THAT(group1.group_token.group_id, Eq(group_id1));
EXPECT_THAT(group2.display_name, Eq(display_name2));
EXPECT_THAT(group2.group_token.group_id, Eq(group_id2));
}
TEST_F(DataSharingServiceImplTest, ShouldInviteMember) {
// TODO(crbug.com/301390275): add a version of this test for unhappy paths.
const GroupId group_id =
not_owned_sdk_delegate_->AddGroupAndReturnId("display_name");
const std::string email = "user@gmail.com";
const std::string gaia_id = "123456789";
not_owned_sdk_delegate_->AddAccount(email, gaia_id);
base::RunLoop run_loop;
base::MockOnceCallback<void(DataSharingService::PeopleGroupActionOutcome)>
callback;
EXPECT_CALL(callback,
Run(DataSharingService::PeopleGroupActionOutcome::kSuccess))
.WillOnce(RunClosure(run_loop.QuitClosure()));
data_sharing_service_->InviteMember(group_id, email, callback.Get());
run_loop.Run();
auto group = not_owned_sdk_delegate_->GetGroup(group_id);
ASSERT_TRUE(group.has_value());
ASSERT_THAT(group->members().size(), Eq(1));
EXPECT_THAT(group->members(0).gaia_id(), Eq(gaia_id));
}
TEST_F(DataSharingServiceImplTest, ShouldRemoveMember) {
// TODO(crbug.com/301390275): add a version of this test for unhappy paths.
const GroupId group_id =
not_owned_sdk_delegate_->AddGroupAndReturnId("display_name");
const std::string email = "user@gmail.com";
const std::string gaia_id = "123456789";
not_owned_sdk_delegate_->AddAccount(email, gaia_id);
not_owned_sdk_delegate_->AddMember(group_id, gaia_id);
base::RunLoop run_loop;
base::MockOnceCallback<void(DataSharingService::PeopleGroupActionOutcome)>
callback;
EXPECT_CALL(callback,
Run(DataSharingService::PeopleGroupActionOutcome::kSuccess))
.WillOnce(RunClosure(run_loop.QuitClosure()));
data_sharing_service_->RemoveMember(group_id, email, callback.Get());
run_loop.Run();
auto group = not_owned_sdk_delegate_->GetGroup(group_id);
ASSERT_TRUE(group.has_value());
EXPECT_TRUE(group->members().empty());
}
TEST_F(DataSharingServiceImplTest, ShouldNotifyObserverOnGroupAddition) {
const std::string display_name = "display_name";
const GroupId group_id =
not_owned_sdk_delegate_->AddGroupAndReturnId(display_name);
base::RunLoop run_loop;
testing::NiceMock<MockObserver> observer;
data_sharing_service_->AddObserver(&observer);
EXPECT_CALL(observer, OnGroupAdded(HasDisplayName(display_name)))
.WillOnce(RunClosure(run_loop.QuitClosure()));
// Mimics initial sync for collaboration group datatype, this should trigger
// OnGroupAdded() notification.
auto* collaboration_group_bridge =
data_sharing_service_->GetCollaborationGroupSyncBridgeForTesting();
syncer::EntityChangeList entity_changes;
entity_changes.push_back(
EntityChangeAddFromSpecifics(MakeCollaborationGroupSpecifics(group_id)));
collaboration_group_bridge->MergeFullSyncData(
collaboration_group_bridge->CreateMetadataChangeList(),
std::move(entity_changes));
run_loop.Run();
}
TEST_F(DataSharingServiceImplTest, ShouldNotifyObserverOnGroupRemoval) {
const GroupId group_id =
not_owned_sdk_delegate_->AddGroupAndReturnId("display_name");
auto* collaboration_group_bridge =
data_sharing_service_->GetCollaborationGroupSyncBridgeForTesting();
// Mimics initial sync for collaboration group datatype.
{
syncer::EntityChangeList entity_changes;
entity_changes.push_back(EntityChangeAddFromSpecifics(
MakeCollaborationGroupSpecifics(group_id)));
collaboration_group_bridge->MergeFullSyncData(
collaboration_group_bridge->CreateMetadataChangeList(),
std::move(entity_changes));
}
// Mimics group removal.
testing::NiceMock<MockObserver> observer;
data_sharing_service_->AddObserver(&observer);
EXPECT_CALL(observer, OnGroupRemoved(group_id));
not_owned_sdk_delegate_->RemoveGroup(group_id);
{
syncer::EntityChangeList entity_changes;
entity_changes.push_back(EntityChangeDeleteFromSpecifics(
MakeCollaborationGroupSpecifics(group_id)));
collaboration_group_bridge->ApplyIncrementalSyncChanges(
collaboration_group_bridge->CreateMetadataChangeList(),
std::move(entity_changes));
}
}
TEST_F(DataSharingServiceImplTest, ShouldNotifyObserverOnGroupChange) {
const GroupId group_id =
not_owned_sdk_delegate_->AddGroupAndReturnId("display_name");
auto* collaboration_group_bridge =
data_sharing_service_->GetCollaborationGroupSyncBridgeForTesting();
// Mimics initial sync for collaboration group datatype.
{
syncer::EntityChangeList entity_changes;
entity_changes.push_back(EntityChangeAddFromSpecifics(
MakeCollaborationGroupSpecifics(group_id)));
collaboration_group_bridge->MergeFullSyncData(
collaboration_group_bridge->CreateMetadataChangeList(),
std::move(entity_changes));
}
// Mimics group update.
base::RunLoop run_loop;
testing::NiceMock<MockObserver> observer;
data_sharing_service_->AddObserver(&observer);
const std::string new_display_name = "new_display_name";
EXPECT_CALL(observer, OnGroupChanged(HasDisplayName(new_display_name)))
.WillOnce(RunClosure(run_loop.QuitClosure()));
not_owned_sdk_delegate_->UpdateGroup(group_id, new_display_name);
{
syncer::EntityChangeList entity_changes;
entity_changes.push_back(EntityChangeUpdateFromSpecifics(
MakeCollaborationGroupSpecifics(group_id)));
collaboration_group_bridge->ApplyIncrementalSyncChanges(
collaboration_group_bridge->CreateMetadataChangeList(),
std::move(entity_changes));
}
run_loop.Run();
}
TEST_F(DataSharingServiceImplTest, ParseDataSharingURL) {
GroupData group_data = GroupData();
group_data.group_token =
GroupToken(data_sharing::GroupId(kGroupId), kTokenBlob);
GURL url = GURL(data_sharing::features::kDataSharingURL.Get() +
"?group_id=" + kGroupId + "&token_blob=" + kTokenBlob);
DataSharingService::ParseURLResult result =
data_sharing_service_->ParseDataSharingURL(url);
// Verify valid path.
EXPECT_TRUE(result.has_value());
EXPECT_EQ(group_data.group_token.group_id.value(),
result.value().group_id.value());
EXPECT_EQ(group_data.group_token.access_token, result.value().access_token);
// Verify host/path error.
std::string invalid = "https://www.test.com/";
url = GURL(invalid + "?group_id=" + kGroupId + "&token_blob=" + kTokenBlob);
result = data_sharing_service_->ParseDataSharingURL(url);
EXPECT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
DataSharingService::ParseURLStatus::kHostOrPathMismatchFailure);
// Verify query missing error.
url = GURL(data_sharing::features::kDataSharingURL.Get() +
"?group_id=" + kGroupId);
result = data_sharing_service_->ParseDataSharingURL(url);
EXPECT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
DataSharingService::ParseURLStatus::kQueryMissingFailure);
}
TEST_F(DataSharingServiceImplTest, GetDataSharingURL) {
GroupData group_data = GroupData();
group_data.group_token =
GroupToken(data_sharing::GroupId(kGroupId), kTokenBlob);
GURL url = GURL(data_sharing::features::kDataSharingURL.Get() + "?group_id=" +
kEncodedGroupId + "&token_blob=" + kEncodedTokenBlob);
std::unique_ptr<GURL> result_url =
data_sharing_service_->GetDataSharingURL(group_data);
// Verify valid path.
EXPECT_TRUE(result_url);
EXPECT_EQ(url, *result_url);
// Verify invalid group data.
result_url = data_sharing_service_->GetDataSharingURL(GroupData());
EXPECT_FALSE(result_url);
}
} // namespace data_sharing