// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/phonehub/browser_tabs_model_provider_impl.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_command_line.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/ash/sync/synced_session_client_ash.h"
#include "chromeos/ash/components/multidevice/remote_device_test_util.h"
#include "chromeos/ash/components/phonehub/fake_browser_tabs_metadata_fetcher.h"
#include "chromeos/ash/components/phonehub/mutable_phone_model.h"
#include "chromeos/ash/components/phonehub/phone_model_test_util.h"
#include "chromeos/ash/components/standalone_browser/lacros_availability.h"
#include "chromeos/ash/components/standalone_browser/standalone_browser_features.h"
#include "chromeos/ash/services/multidevice_setup/public/cpp/fake_multidevice_setup_client.h"
#include "chromeos/crosapi/mojom/synced_session_client.mojom.h"
#include "components/account_id/account_id.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/policy_constants.h"
#include "components/sync/base/features.h"
#include "components/sync/test/mock_sync_service.h"
#include "components/sync_sessions/open_tabs_ui_delegate.h"
#include "components/sync_sessions/session_sync_service.h"
#include "components/user_manager/scoped_user_manager.h"
#include "components/user_manager/user.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace ash {
namespace phonehub {
namespace {
using ::testing::_;
constexpr char kPhoneNameOne[] = "Pixel";
constexpr char kPhoneNameTwo[] = "Galaxy";
constexpr char kManagedUserEmail[] = "[email protected]";
constexpr base::Time kRecentSessionTime = base::Time::FromTimeT(100);
constexpr base::Time kOlderSessionTime = base::Time::FromTimeT(10);
class SessionSyncServiceMock : public sync_sessions::SessionSyncService {
public:
SessionSyncServiceMock() {}
~SessionSyncServiceMock() override {}
MOCK_CONST_METHOD0(GetGlobalIdMapper, syncer::GlobalIdMapper*());
MOCK_METHOD0(GetOpenTabsUIDelegate, sync_sessions::OpenTabsUIDelegate*());
MOCK_METHOD1(
SubscribeToForeignSessionsChanged,
base::CallbackListSubscription(const base::RepeatingClosure& cb));
MOCK_METHOD0(ScheduleGarbageCollection, void());
MOCK_METHOD0(GetControllerDelegate,
base::WeakPtr<syncer::DataTypeControllerDelegate>());
};
class OpenTabsUIDelegateMock : public sync_sessions::OpenTabsUIDelegate {
public:
OpenTabsUIDelegateMock() {}
~OpenTabsUIDelegateMock() override {}
MOCK_METHOD1(GetAllForeignSessions,
bool(std::vector<raw_ptr<const sync_sessions::SyncedSession,
VectorExperimental>>* sessions));
MOCK_METHOD3(GetForeignTab,
bool(const std::string& tag,
const SessionID tab_id,
const sessions::SessionTab** tab));
MOCK_METHOD1(DeleteForeignSession, void(const std::string& tag));
MOCK_METHOD1(
GetForeignSession,
std::vector<const sessions::SessionWindow*>(const std::string& tag));
MOCK_METHOD2(GetForeignSessionTabs,
bool(const std::string& tag,
std::vector<const sessions::SessionTab*>* tabs));
MOCK_METHOD1(GetLocalSession,
bool(const sync_sessions::SyncedSession** local));
};
// This class wraps the setup of a Lacros Only environment and makes it easy to
// reset the state after use by destroying the handle.
class ScopedLacrosOnlyHandle {
public:
ScopedLacrosOnlyHandle() {
auto fake_user_manager = std::make_unique<ash::FakeChromeUserManager>();
fake_user_manager_ = fake_user_manager.get();
scoped_user_manager_ = std::make_unique<user_manager::ScopedUserManager>(
std::move(fake_user_manager));
SetLoggedInUser();
SetLacrosAvailability();
}
~ScopedLacrosOnlyHandle() { ResetLacrosAvailability(); }
private:
void SetLoggedInUser() {
AccountId account_id = AccountId::FromUserEmail(kManagedUserEmail);
const user_manager::User* user = fake_user_manager_->AddUser(account_id);
fake_user_manager_->UserLoggedIn(account_id, user->username_hash(),
/*browser_restart=*/false,
/*is_child=*/false);
}
void SetLacrosAvailability() {
policy::PolicyMap policy;
policy.Set(policy::key::kLacrosAvailability, policy::POLICY_LEVEL_MANDATORY,
policy::POLICY_SCOPE_USER, policy::POLICY_SOURCE_CLOUD,
base::Value(GetLacrosAvailabilityPolicyName(
standalone_browser::LacrosAvailability::kLacrosOnly)),
/*external_data_fetcher=*/nullptr);
crosapi::browser_util::CacheLacrosAvailability(policy);
}
void ResetLacrosAvailability() {
crosapi::browser_util::ClearLacrosAvailabilityCacheForTest();
}
raw_ptr<ash::FakeChromeUserManager, DanglingUntriaged> fake_user_manager_ =
nullptr;
std::unique_ptr<user_manager::ScopedUserManager> scoped_user_manager_;
};
multidevice::RemoteDeviceRef CreatePhoneDevice(const std::string& pii_name) {
multidevice::RemoteDeviceRefBuilder builder;
builder.SetPiiFreeName(pii_name);
return builder.Build();
}
std::unique_ptr<sync_sessions::SyncedSession> CreateNewSession(
const std::string& session_name,
const base::Time& session_time = base::Time::FromSecondsSinceUnixEpoch(0)) {
auto session = std::make_unique<sync_sessions::SyncedSession>();
session->SetSessionName(session_name);
session->SetModifiedTime(session_time);
return session;
}
std::vector<crosapi::mojom::SyncedSessionPtr>
CreateTestSyncedSessionsNoMatchingSession() {
// This is the most recent session for Galaxy session.
std::vector<crosapi::mojom::SyncedSessionPtr> sessions;
crosapi::mojom::SyncedSessionPtr session =
crosapi::mojom::SyncedSession::New();
session->session_name = kPhoneNameTwo;
session->modified_time = kRecentSessionTime;
sessions.push_back(std::move(session));
return sessions;
}
std::vector<crosapi::mojom::SyncedSessionPtr> CreateTestSyncedSessions() {
std::vector<crosapi::mojom::SyncedSessionPtr> sessions;
// This is the most recent session for Pixel session.
crosapi::mojom::SyncedSessionPtr session1 =
crosapi::mojom::SyncedSession::New();
session1->session_name = kPhoneNameOne;
session1->modified_time = kRecentSessionTime;
sessions.push_back(std::move(session1));
// This is the most recent session for Galaxy session.
crosapi::mojom::SyncedSessionPtr session2 =
crosapi::mojom::SyncedSession::New();
session2->session_name = kPhoneNameTwo;
session2->modified_time = kRecentSessionTime;
sessions.push_back(std::move(session2));
// This is an older session for Pixel session.
crosapi::mojom::SyncedSessionPtr session3 =
crosapi::mojom::SyncedSession::New();
session3->session_name = kPhoneNameOne;
session3->modified_time = kOlderSessionTime;
sessions.push_back(std::move(session3));
return sessions;
}
} // namespace
class BrowserTabsModelProviderImplTest
: public testing::Test,
public BrowserTabsModelProvider::Observer {
public:
BrowserTabsModelProviderImplTest() = default;
BrowserTabsModelProviderImplTest(const BrowserTabsModelProviderImplTest&) =
delete;
BrowserTabsModelProviderImplTest& operator=(
const BrowserTabsModelProviderImplTest&) = delete;
~BrowserTabsModelProviderImplTest() override = default;
// BrowserTabsModelProvider::Observer:
void OnBrowserTabsUpdated(
bool is_sync_enabled,
const std::vector<BrowserTabsModel::BrowserTabMetadata>&
browser_tabs_metadata) override {
phone_model_.SetBrowserTabsModel(BrowserTabsModel(
/*is_tab_sync_enabled=*/is_sync_enabled, browser_tabs_metadata));
}
// testing::Test:
void SetUp() override {
ON_CALL(mock_session_sync_service_, GetOpenTabsUIDelegate())
.WillByDefault(Invoke(
this, &BrowserTabsModelProviderImplTest::open_tabs_ui_delegate));
ON_CALL(open_tabs_ui_delegate_, GetAllForeignSessions(_))
.WillByDefault(Invoke(
this,
&BrowserTabsModelProviderImplTest::MockGetAllForeignSessions));
ON_CALL(mock_session_sync_service_, SubscribeToForeignSessionsChanged(_))
.WillByDefault(Invoke(this, &BrowserTabsModelProviderImplTest::
MockSubscribeToForeignSessionsChanged));
}
void CreateProvider() {
provider_ = std::make_unique<BrowserTabsModelProviderImpl>(
&fake_multidevice_setup_client_, &synced_session_client_ash_,
&mock_sync_service_, &mock_session_sync_service_,
std::make_unique<FakeBrowserTabsMetadataFetcher>());
provider_->AddObserver(this);
}
void SetPiiFreeName(const std::string& pii_free_name) {
fake_multidevice_setup_client_.SetHostStatusWithDevice(std::make_pair(
multidevice_setup::mojom::HostStatus::kEligibleHostExistsButNoHostSet,
CreatePhoneDevice(/*pii_name=*/pii_free_name)));
}
base::CallbackListSubscription MockSubscribeToForeignSessionsChanged(
const base::RepeatingClosure& cb) {
foreign_sessions_changed_callback_ = std::move(cb);
return {};
}
bool MockGetAllForeignSessions(
std::vector<raw_ptr<const sync_sessions::SyncedSession,
VectorExperimental>>* sessions) {
if (sessions_) {
*sessions = *sessions_;
return !sessions->empty();
}
return false;
}
testing::NiceMock<OpenTabsUIDelegateMock>* open_tabs_ui_delegate() {
return enable_tab_sync_ ? &open_tabs_ui_delegate_ : nullptr;
}
void NotifySubscription() { foreign_sessions_changed_callback_.Run(); }
void OnForeignSyncedPhoneSessionsUpdated(
std::vector<crosapi::mojom::SyncedSessionPtr> sessions) {
synced_session_client_ash_.OnForeignSyncedPhoneSessionsUpdated(
std::move(sessions));
}
void OnSessionSyncEnabledChanged(bool enabled) {
synced_session_client_ash_.OnSessionSyncEnabledChanged(enabled);
}
void set_synced_sessions(
std::vector<raw_ptr<const sync_sessions::SyncedSession,
VectorExperimental>>* sessions) {
sessions_ = sessions;
}
void set_enable_tab_sync(bool is_enabled) { enable_tab_sync_ = is_enabled; }
FakeBrowserTabsMetadataFetcher* fake_browser_tabs_metadata_fetcher() {
return static_cast<FakeBrowserTabsMetadataFetcher*>(
provider_->browser_tabs_metadata_fetcher_.get());
}
MutablePhoneModel phone_model_;
multidevice_setup::FakeMultiDeviceSetupClient fake_multidevice_setup_client_;
SyncedSessionClientAsh synced_session_client_ash_;
testing::NiceMock<syncer::MockSyncService> mock_sync_service_;
testing::NiceMock<SessionSyncServiceMock> mock_session_sync_service_;
std::unique_ptr<BrowserTabsModelProviderImpl> provider_;
testing::NiceMock<OpenTabsUIDelegateMock> open_tabs_ui_delegate_;
bool enable_tab_sync_ = true;
raw_ptr<std::vector<
raw_ptr<const sync_sessions::SyncedSession, VectorExperimental>>>
sessions_ = nullptr;
base::RepeatingClosure foreign_sessions_changed_callback_;
};
TEST_F(BrowserTabsModelProviderImplTest, AttemptBrowserTabsModelUpdate) {
CreateProvider();
// Test no Pii Free name despite sync being enabled.
set_enable_tab_sync(true);
set_synced_sessions(nullptr);
NotifySubscription();
EXPECT_FALSE(phone_model_.browser_tabs_model()->is_tab_sync_enabled());
EXPECT_TRUE(phone_model_.browser_tabs_model()->most_recent_tabs().empty());
EXPECT_FALSE(
fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
// Set name of phone. Tests that OnHostStatusChanged causes name change.
SetPiiFreeName(kPhoneNameOne);
// Test disabling tab sync with no browser tab metadata.
set_enable_tab_sync(false);
set_synced_sessions(nullptr);
NotifySubscription();
EXPECT_FALSE(phone_model_.browser_tabs_model()->is_tab_sync_enabled());
EXPECT_TRUE(phone_model_.browser_tabs_model()->most_recent_tabs().empty());
EXPECT_FALSE(
fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
// Test enabling tab sync with no browser tab metadata.
set_enable_tab_sync(true);
set_synced_sessions(nullptr);
NotifySubscription();
EXPECT_TRUE(phone_model_.browser_tabs_model()->is_tab_sync_enabled());
EXPECT_TRUE(phone_model_.browser_tabs_model()->most_recent_tabs().empty());
EXPECT_FALSE(
fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
// Test enabling tab sync with no matching pii name with session_name.
std::vector<raw_ptr<const sync_sessions::SyncedSession, VectorExperimental>>
sessions;
std::unique_ptr<sync_sessions::SyncedSession> session =
CreateNewSession(kPhoneNameTwo);
sessions.emplace_back(session.get());
set_enable_tab_sync(true);
set_synced_sessions(&sessions);
NotifySubscription();
EXPECT_TRUE(phone_model_.browser_tabs_model()->is_tab_sync_enabled());
EXPECT_TRUE(phone_model_.browser_tabs_model()->most_recent_tabs().empty());
EXPECT_FALSE(
fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
// Test enabling tab sync with matching pii name with session_name, which
// will cause the |fake_browser_tabs_metadata_fetcher()| to have a pending
// callback.
std::unique_ptr<sync_sessions::SyncedSession> new_session =
CreateNewSession(kPhoneNameOne);
sessions.emplace_back(new_session.get());
set_enable_tab_sync(true);
set_synced_sessions(&sessions);
NotifySubscription();
EXPECT_TRUE(phone_model_.browser_tabs_model()->is_tab_sync_enabled());
EXPECT_TRUE(phone_model_.browser_tabs_model()->most_recent_tabs().empty());
EXPECT_TRUE(fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
// Test that once |fake_browser_tabs_metadata_fetcher()| responds, the phone
// model will be appropriately updated.
std::vector<BrowserTabsModel::BrowserTabMetadata> metadata;
metadata.push_back(CreateFakeBrowserTabMetadata());
fake_browser_tabs_metadata_fetcher()->RespondToCurrentFetchAttempt(
std::move(metadata));
EXPECT_EQ(phone_model_.browser_tabs_model()->most_recent_tabs().size(), 1U);
}
TEST_F(BrowserTabsModelProviderImplTest, OnForeignSyncedPhoneSessionsUpdated) {
// Enable "Lacros Only" by setting feature flags and creating a handle which
// sets the logged-in user and the Lacros availability policy.
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
/*enabled_features=*/{syncer::kChromeOSSyncedSessionSharing},
/*disabled_features=*/{});
base::CommandLine::ForCurrentProcess()->AppendSwitch(
ash::switches::kEnableLacrosForTesting);
ScopedLacrosOnlyHandle lacros_only_handle;
CreateProvider();
OnSessionSyncEnabledChanged(true);
// Test no Pii Free name.
OnForeignSyncedPhoneSessionsUpdated({});
ASSERT_TRUE(phone_model_.browser_tabs_model().has_value());
EXPECT_FALSE(phone_model_.browser_tabs_model()->is_tab_sync_enabled());
EXPECT_TRUE(phone_model_.browser_tabs_model()->most_recent_tabs().empty());
EXPECT_FALSE(
fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
// Set name of phone. Tests that OnHostStatusChanged causes name change.
SetPiiFreeName(kPhoneNameOne);
// Test tab sync with no foreign synced sessions updated.
OnForeignSyncedPhoneSessionsUpdated({});
ASSERT_TRUE(phone_model_.browser_tabs_model().has_value());
EXPECT_TRUE(phone_model_.browser_tabs_model()->is_tab_sync_enabled());
EXPECT_TRUE(phone_model_.browser_tabs_model()->most_recent_tabs().empty());
EXPECT_FALSE(
fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
// Test tab sync with foreign synced sessions that do not match session.
OnForeignSyncedPhoneSessionsUpdated(
CreateTestSyncedSessionsNoMatchingSession());
ASSERT_TRUE(phone_model_.browser_tabs_model().has_value());
EXPECT_TRUE(phone_model_.browser_tabs_model()->is_tab_sync_enabled());
EXPECT_FALSE(
fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
// Test tab sync with foreign synced sessions updated.
OnForeignSyncedPhoneSessionsUpdated(CreateTestSyncedSessions());
ASSERT_TRUE(phone_model_.browser_tabs_model().has_value());
EXPECT_TRUE(phone_model_.browser_tabs_model()->is_tab_sync_enabled());
EXPECT_TRUE(fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
// TODO(b/260599791): Move ForeignSyncedSessionAsh to a directory that can
// be imported by FakeBrowserTabsMetadataFetcher and verify that the
// correct session is being passed.
}
TEST_F(BrowserTabsModelProviderImplTest, OnSessionSyncEnabledChanged) {
// Enable "Lacros Only" by setting feature flags and creating a handle which
// sets the logged-in user and the Lacros availability policy.
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
/*enabled_features=*/{syncer::kChromeOSSyncedSessionSharing},
/*disabled_features=*/{});
base::CommandLine::ForCurrentProcess()->AppendSwitch(
ash::switches::kEnableLacrosForTesting);
ScopedLacrosOnlyHandle lacros_only_handle;
CreateProvider();
// Set name of phone and cached sessions.
SetPiiFreeName(kPhoneNameOne);
// If session sync becomes enabled with no sessions available, metadata
// fetcher is not invoked.
OnSessionSyncEnabledChanged(true);
ASSERT_TRUE(phone_model_.browser_tabs_model().has_value());
EXPECT_TRUE(phone_model_.browser_tabs_model()->is_tab_sync_enabled());
EXPECT_FALSE(
fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
// Trigger an update and respond to the fetch request.
OnForeignSyncedPhoneSessionsUpdated(CreateTestSyncedSessions());
EXPECT_TRUE(fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
std::vector<BrowserTabsModel::BrowserTabMetadata> metadata1;
metadata1.push_back(CreateFakeBrowserTabMetadata());
fake_browser_tabs_metadata_fetcher()->RespondToCurrentFetchAttempt(
std::move(metadata1));
ASSERT_TRUE(phone_model_.browser_tabs_model().has_value());
EXPECT_TRUE(phone_model_.browser_tabs_model()->is_tab_sync_enabled());
// If session sync becomes disabled we should reflect this in the model.
OnSessionSyncEnabledChanged(false);
ASSERT_TRUE(phone_model_.browser_tabs_model().has_value());
EXPECT_FALSE(phone_model_.browser_tabs_model()->is_tab_sync_enabled());
EXPECT_FALSE(
fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
// If session sync is re-enabled, we should invoke the metadata fetcher.
OnSessionSyncEnabledChanged(true);
EXPECT_TRUE(fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
std::vector<BrowserTabsModel::BrowserTabMetadata> metadata2;
metadata2.push_back(CreateFakeBrowserTabMetadata());
fake_browser_tabs_metadata_fetcher()->RespondToCurrentFetchAttempt(
std::move(metadata2));
ASSERT_TRUE(phone_model_.browser_tabs_model().has_value());
EXPECT_TRUE(phone_model_.browser_tabs_model()->is_tab_sync_enabled());
}
TEST_F(BrowserTabsModelProviderImplTest, ClearTabMetadataDuringMetadataFetch) {
CreateProvider();
SetPiiFreeName(kPhoneNameOne);
std::unique_ptr<sync_sessions::SyncedSession> new_session =
CreateNewSession(kPhoneNameOne);
std::vector<raw_ptr<const sync_sessions::SyncedSession, VectorExperimental>>
sessions({new_session.get()});
set_enable_tab_sync(true);
set_synced_sessions(&sessions);
NotifySubscription();
EXPECT_TRUE(fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
// Set to no synced sessions. Tab sync is still enabled.
set_synced_sessions(nullptr);
NotifySubscription();
// Test that if the Browser tab metadata is cleared while a browser tab
// metadata fetch is in progress, the in progress callback will be cancelled.
std::vector<BrowserTabsModel::BrowserTabMetadata> metadata;
metadata.push_back(CreateFakeBrowserTabMetadata());
fake_browser_tabs_metadata_fetcher()->RespondToCurrentFetchAttempt(
std::move(metadata));
EXPECT_TRUE(phone_model_.browser_tabs_model()->most_recent_tabs().empty());
}
TEST_F(BrowserTabsModelProviderImplTest, SessionCorrectlySelected) {
CreateProvider();
SetPiiFreeName(kPhoneNameOne);
std::unique_ptr<sync_sessions::SyncedSession> session_a =
CreateNewSession(kPhoneNameOne, base::Time::FromSecondsSinceUnixEpoch(1));
std::unique_ptr<sync_sessions::SyncedSession> session_b =
CreateNewSession(kPhoneNameOne, base::Time::FromSecondsSinceUnixEpoch(3));
std::unique_ptr<sync_sessions::SyncedSession> session_c =
CreateNewSession(kPhoneNameOne, base::Time::FromSecondsSinceUnixEpoch(2));
std::unique_ptr<sync_sessions::SyncedSession> session_d = CreateNewSession(
kPhoneNameTwo, base::Time::FromSecondsSinceUnixEpoch(10));
std::vector<raw_ptr<const sync_sessions::SyncedSession, VectorExperimental>>
sessions(
{session_a.get(), session_b.get(), session_c.get(), session_d.get()});
set_enable_tab_sync(true);
set_synced_sessions(&sessions);
NotifySubscription();
EXPECT_TRUE(fake_browser_tabs_metadata_fetcher()->DoesPendingCallbackExist());
// |session_b| should be the selected session because it is the has the same
// session_name as the set phone name and the latest modified time.
EXPECT_EQ(fake_browser_tabs_metadata_fetcher()->GetSession(),
session_b.get());
}
} // namespace phonehub
} // namespace ash