// 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 "chrome/browser/lacros/sync/crosapi_session_sync_notifier.h"
#include <string_view>
#include <utility>
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/task_environment.h"
#include "chromeos/crosapi/mojom/sync.mojom.h"
#include "chromeos/crosapi/mojom/synced_session_client.mojom.h"
#include "components/sessions/core/serialized_navigation_entry_test_helper.h"
#include "components/sync/test/fake_synced_session_client_ash.h"
#include "components/sync/test/test_sync_service.h"
#include "components/sync_sessions/mock_sync_sessions_client.h"
#include "components/sync_sessions/open_tabs_ui_delegate_impl.h"
#include "components/sync_sessions/session_sync_service.h"
#include "components/sync_sessions/synced_session.h"
#include "components/sync_sessions/synced_session_tracker.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
using testing::_;
using testing::Return;
constexpr char kSessionTag1[] = "foreign1";
constexpr char kSessionTag2[] = "foreign2";
constexpr char kSessionTag3[] = "foreign3";
constexpr SessionID kWindowId1 = SessionID::FromSerializedValue(1);
constexpr SessionID kWindowId2 = SessionID::FromSerializedValue(2);
constexpr SessionID kWindowId3 = SessionID::FromSerializedValue(3);
constexpr SessionID kTabId1 = SessionID::FromSerializedValue(111);
constexpr SessionID kTabId2 = SessionID::FromSerializedValue(222);
constexpr SessionID kTabId3 = SessionID::FromSerializedValue(333);
// TODO(b/272291842): use a FakeSessionSyncService in place of this mock.
class MockSessionSyncService : public sync_sessions::SessionSyncService {
public:
MockSessionSyncService() = default;
~MockSessionSyncService() override = default;
MOCK_METHOD(syncer::GlobalIdMapper*,
GetGlobalIdMapper,
(),
(const, override));
MOCK_METHOD(sync_sessions::OpenTabsUIDelegate*,
GetOpenTabsUIDelegate,
(),
(override));
MOCK_METHOD(base::CallbackListSubscription,
SubscribeToForeignSessionsChanged,
(const base::RepeatingClosure& cb),
(override));
MOCK_METHOD(base::WeakPtr<syncer::DataTypeControllerDelegate>,
GetControllerDelegate,
());
};
} // namespace
class CrosapiSessionSyncNotifierTest : public testing::Test {
public:
CrosapiSessionSyncNotifierTest()
: synced_session_tracker_(&mock_sync_sessions_client_),
open_tabs_ui_delegate_(
&mock_sync_sessions_client_,
&synced_session_tracker_,
base::BindRepeating(
&CrosapiSessionSyncNotifierTest::DeleteForeignSessionCallback,
base::Unretained(this))) {}
void SetUp() override {
ON_CALL(mock_session_sync_service_, SubscribeToForeignSessionsChanged(_))
.WillByDefault(Invoke(this, &CrosapiSessionSyncNotifierTest::
SubscribeToForeignSessionsChanged));
ON_CALL(mock_session_sync_service_, GetOpenTabsUIDelegate())
.WillByDefault(Invoke(
this, &CrosapiSessionSyncNotifierTest::GetOpenTabsUIDelegate));
// All SessionTabs are considered to have interesting URLs. Allows
// `SyncedSessionTracker::LookupAllForeignSessions()` to be passed
// `SessionLookup::PRESENTABLE` in
// `MockOpenTabsUIDelegate::GetAllForeignSessions()` just like it is in
// `OpenTabsUIDelegate::GetAllForeignSessions()`.
ON_CALL(mock_sync_sessions_client_, ShouldSyncURL(_))
.WillByDefault(Return(true));
test_sync_service_ = std::make_unique<syncer::TestSyncService>();
test_sync_service_->GetUserSettings()->SetSelectedTypes(
/*sync_everything=*/false, /*types=*/{});
// Create object under test.
crosapi_session_sync_notifier_ =
std::make_unique<CrosapiSessionSyncNotifier>(
&mock_session_sync_service_,
fake_synced_session_client_ash_.CreateRemote(),
test_sync_service_.get(), /*favicon_request_handler=*/nullptr);
}
base::CallbackListSubscription SubscribeToForeignSessionsChanged(
const base::RepeatingClosure& cb) {
foreign_sessions_changed_callback_ = cb;
return {};
}
sync_sessions::OpenTabsUIDelegate* GetOpenTabsUIDelegate() {
return &open_tabs_ui_delegate_;
}
bool GetAllForeignSessions(
std::vector<raw_ptr<const sync_sessions::SyncedSession,
VectorExperimental>>* sessions) {
foreign_sessions_ = synced_session_tracker_.LookupAllForeignSessions(
sync_sessions::SyncedSessionTracker::SessionLookup::PRESENTABLE);
*sessions = foreign_sessions_;
return !sessions->empty();
}
void NotifyForeignSessionsChanged() {
foreign_sessions_changed_callback_.Run();
}
// Creates a new SessionTab with id `tab_id` inside of a SessionWindow with id
// `window_id` inside of a SyncedSession with tag `session_tag`. If a
// SessionTab with id `tab_id` already exists within a SyncedSession with tag
// `session_tag` and SessionWindow with id `window_id`, this function returns
// false, and no new objects are created. Otherwise, the function returns
// true. If no SyncedSession and/or SessionWindow exist with their respective
// tag/id, then a new one is created in order to create the new SessionTag.
// All SyncedSessions will have device form factor `kPhone`. All SessionTabs
// will have valid urls with https schemes.
bool CreateForeignPhonePresentableTabInSession(
const std::string_view& session_tag,
const SessionID window_id,
const SessionID tab_id) {
sync_sessions::SyncedSession* session =
synced_session_tracker_.GetSession(session_tag.data());
const sessions::SessionTab* tab =
synced_session_tracker_.LookupSessionTab(session_tag.data(), tab_id);
// If a SessionTab with id `tab_id` exists within a SessionWindow with id
// `window_id`, a duplicate is not created.
if (tab && window_id == tab->window_id) {
return false;
}
CreateForeignPhonePresentableTabInWindow(session_tag, window_id, tab_id);
session->SetDeviceTypeAndFormFactor(
sync_pb::SyncEnums_DeviceType_TYPE_PHONE,
syncer::DeviceInfo::FormFactor::kPhone);
return true;
}
// Checks that all tab, window, and session information sent from the
// `CrosapiSessionSyncNotifier` to the `FakeSyncedSessionClient` was received
// exactly as sent, even if the sent message was empty.
void ValidateSentSessions() {
const std::vector<raw_ptr<const sync_sessions::SyncedSession,
VectorExperimental>>& sent_sessions =
synced_session_tracker_.LookupAllForeignSessions(
sync_sessions::SyncedSessionTracker::SessionLookup::PRESENTABLE);
const std::vector<crosapi::mojom::SyncedSessionPtr>& received_sessions =
fake_synced_session_client_ash_.LookupForeignSyncedPhoneSessions();
ASSERT_EQ(sent_sessions.size(), received_sessions.size());
for (size_t idx = 0; idx < sent_sessions.size(); idx++) {
const sync_sessions::SyncedSession& sent_session = *sent_sessions[idx];
const crosapi::mojom::SyncedSession* received_session =
received_sessions[idx].get();
EXPECT_EQ(sent_session.GetSessionName(), received_session->session_name);
EXPECT_EQ(sent_session.GetModifiedTime(),
received_session->modified_time);
ValidateSentWindows(sent_session.GetSessionTag(),
received_session->windows);
}
}
void SetPhoneSessionsUpdatedCallback(base::RepeatingClosure callback) {
fake_synced_session_client_ash_
.SetOnForeignSyncedPhoneSessionsUpdatedCallback(std::move(callback));
}
void SetDeleteForeignSessionCallback(const base::RepeatingClosure& cb) {
delete_foreign_session_callback_ = cb;
}
syncer::TestSyncService* test_sync_service() {
return test_sync_service_.get();
}
syncer::FakeSyncedSessionClientAsh* fake_synced_session_client_ash() {
return &fake_synced_session_client_ash_;
}
CrosapiSessionSyncNotifier* crosapi_session_sync_notifier() {
return crosapi_session_sync_notifier_.get();
}
private:
// Helper to `CreateForeignPhonePresentableTabInSession()`, keeps all promises
// made by that function. Finds the SessionWindow with id `window_id` to
// create a SessionTab in. If none exists, it is created.
void CreateForeignPhonePresentableTabInWindow(
const std::string_view& session_tag,
const SessionID window_id,
const SessionID tab_id) {
std::vector<const sessions::SessionWindow*> windows =
synced_session_tracker_.LookupSessionWindows(session_tag.data());
for (const sessions::SessionWindow* window : windows) {
if (window_id == window->window_id) {
// This can be done without checking for tab existence in the window
// because the tab's existence is checked in the session in
// `CreateForeignPhonePresentableTabInSession()`.
CreateForeignPhonePresentableTab(session_tag.data(), window_id, tab_id);
return;
}
}
// No SessionWindow with tag `window_id` was found in SyncedSession with tag
// `session_tag`, so one is created.
synced_session_tracker_.PutWindowInSession(session_tag.data(), window_id);
CreateForeignPhonePresentableTab(session_tag, window_id, tab_id);
}
// Helper to `CreateForeignPhonePresentableTabInSession()`, keeps all promises
// made by that function. Creates a new SessionTab with id `tab_id`.
void CreateForeignPhonePresentableTab(const std::string_view& session_tag,
const SessionID window_id,
const SessionID tab_id) {
// This can be done without checking for tab existence in the window because
// the tab's existence is checked in the session in
// `CreateForeignPhonePresentableTabInSession()`.
synced_session_tracker_.PutTabInWindow(session_tag.data(), window_id,
tab_id);
sessions::SessionTab* tab =
synced_session_tracker_.GetTab(session_tag.data(), tab_id);
tab->navigations.push_back(sessions::SerializedNavigationEntryTestHelper::
CreateNavigationForTest());
tab->timestamp = base::Time::Now();
}
// Helper to ValidateSentSessions. Validates the members of each window in the
// session with tag `sent_session_tag` against the members of each window in
// `received_windows`.
void ValidateSentWindows(
const std::string& sent_session_tag,
const std::vector<crosapi::mojom::SyncedSessionWindowPtr>&
received_windows) {
std::vector<const sessions::SessionWindow*> sent_windows =
synced_session_tracker_.LookupSessionWindows(sent_session_tag);
EXPECT_EQ(sent_windows.empty(), received_windows.empty());
if (sent_windows.empty()) {
return;
}
ASSERT_EQ(sent_windows.size(), received_windows.size());
for (size_t idx = 0; idx < sent_windows.size(); idx++) {
ValidateSentTabs(sent_windows[idx]->tabs, received_windows[idx]->tabs);
}
}
// Helper to ValidateSentSessions. Validates the members of each tab in
// `sent_tabs` against the members of each tab in `received_tabs`.
void ValidateSentTabs(
const std::vector<std::unique_ptr<sessions::SessionTab>>& sent_tabs,
const std::vector<crosapi::mojom::SyncedSessionTabPtr>& received_tabs) {
ASSERT_EQ(sent_tabs.size(), received_tabs.size());
for (size_t idx = 0; idx < sent_tabs.size(); idx++) {
// Get sent SessionTab, check that the tab sent is valid to show via
// PhoneHub.
const sessions::SessionTab& sent_tab = *sent_tabs[idx];
const int sent_selected_index = sent_tab.normalized_navigation_index();
const sessions::SerializedNavigationEntry& sent_navigation =
sent_tab.navigations[sent_selected_index];
const GURL& sent_tab_url = sent_navigation.virtual_url();
const crosapi::mojom::SyncedSessionTab* received_tab =
received_tabs[idx].get();
EXPECT_EQ(sent_tab_url, received_tab->current_navigation_url);
EXPECT_EQ(sent_navigation.title(),
received_tab->current_navigation_title);
EXPECT_EQ(sent_tab.timestamp, received_tab->last_modified_timestamp);
}
}
void DeleteForeignSessionCallback(const std::string& tag) {
delete_foreign_session_callback_.Run();
}
base::test::SingleThreadTaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
std::unique_ptr<syncer::TestSyncService> test_sync_service_;
std::unique_ptr<CrosapiSessionSyncNotifier> crosapi_session_sync_notifier_;
syncer::FakeSyncedSessionClientAsh fake_synced_session_client_ash_;
testing::NiceMock<sync_sessions::MockSyncSessionsClient>
mock_sync_sessions_client_;
sync_sessions::SyncedSessionTracker synced_session_tracker_;
base::RepeatingClosure delete_foreign_session_callback_;
sync_sessions::OpenTabsUIDelegateImpl open_tabs_ui_delegate_;
testing::NiceMock<MockSessionSyncService> mock_session_sync_service_;
std::vector<raw_ptr<const sync_sessions::SyncedSession, VectorExperimental>>
foreign_sessions_;
base::RepeatingClosure foreign_sessions_changed_callback_;
};
TEST_F(CrosapiSessionSyncNotifierTest,
OnForeignSyncedPhoneSessionsUpdated_OneTab) {
base::RunLoop run_loop;
SetPhoneSessionsUpdatedCallback(run_loop.QuitClosure());
EXPECT_TRUE(CreateForeignPhonePresentableTabInSession(kSessionTag1,
kWindowId1, kTabId1));
NotifyForeignSessionsChanged();
run_loop.Run();
ValidateSentSessions();
}
TEST_F(CrosapiSessionSyncNotifierTest,
OnForeignSyncedPhoneSessionsUpdated_OneSession_MultipleTabs) {
base::RunLoop run_loop;
SetPhoneSessionsUpdatedCallback(run_loop.QuitClosure());
EXPECT_TRUE(CreateForeignPhonePresentableTabInSession(kSessionTag1,
kWindowId1, kTabId1));
EXPECT_TRUE(CreateForeignPhonePresentableTabInSession(kSessionTag1,
kWindowId1, kTabId2));
EXPECT_TRUE(CreateForeignPhonePresentableTabInSession(kSessionTag1,
kWindowId1, kTabId3));
NotifyForeignSessionsChanged();
run_loop.Run();
ValidateSentSessions();
}
TEST_F(CrosapiSessionSyncNotifierTest,
OnForeignSyncedPhoneSessionsUpdated_MultipleSessions_MultipleTabs) {
base::RunLoop run_loop;
SetPhoneSessionsUpdatedCallback(run_loop.QuitClosure());
EXPECT_TRUE(CreateForeignPhonePresentableTabInSession(kSessionTag1,
kWindowId1, kTabId1));
EXPECT_TRUE(CreateForeignPhonePresentableTabInSession(kSessionTag1,
kWindowId2, kTabId2));
EXPECT_TRUE(CreateForeignPhonePresentableTabInSession(kSessionTag1,
kWindowId3, kTabId3));
EXPECT_TRUE(CreateForeignPhonePresentableTabInSession(kSessionTag2,
kWindowId1, kTabId1));
EXPECT_TRUE(CreateForeignPhonePresentableTabInSession(kSessionTag2,
kWindowId2, kTabId2));
EXPECT_TRUE(CreateForeignPhonePresentableTabInSession(kSessionTag2,
kWindowId3, kTabId3));
EXPECT_TRUE(CreateForeignPhonePresentableTabInSession(kSessionTag3,
kWindowId1, kTabId1));
EXPECT_TRUE(CreateForeignPhonePresentableTabInSession(kSessionTag3,
kWindowId2, kTabId2));
EXPECT_TRUE(CreateForeignPhonePresentableTabInSession(kSessionTag3,
kWindowId3, kTabId3));
NotifyForeignSessionsChanged();
run_loop.Run();
ValidateSentSessions();
}
TEST_F(CrosapiSessionSyncNotifierTest,
OnForeignSyncedPhoneSessionsUpdated_NoSessions) {
base::RunLoop run_loop;
SetPhoneSessionsUpdatedCallback(run_loop.QuitClosure());
NotifyForeignSessionsChanged();
run_loop.Run();
ValidateSentSessions();
}
TEST_F(CrosapiSessionSyncNotifierTest, SyncServiceObserverAdded) {
EXPECT_TRUE(
test_sync_service()->HasObserver(crosapi_session_sync_notifier()));
}
TEST_F(CrosapiSessionSyncNotifierTest, OnStateChanged_NoChangeToStartingValue) {
// OnStateChanged() is called from the CrosapiSessionSyncNotifier constructor.
// The "tab sync enabled" value should remain |false|
test_sync_service()->GetUserSettings()->SetSelectedTypes(
/*sync_everything=*/false, /*types=*/{});
test_sync_service()->FireStateChanged();
EXPECT_FALSE(fake_synced_session_client_ash()->is_session_sync_enabled());
}
TEST_F(CrosapiSessionSyncNotifierTest,
OnStateChanged_TabSyncEnabledStateChanged) {
// OnStateChange() is called if the "tab sync enabled" value changes
test_sync_service()->GetUserSettings()->SetSelectedTypes(
/*sync_everything=*/true, /*types=*/{});
test_sync_service()->FireStateChanged();
fake_synced_session_client_ash()->FlushMojoForTesting();
EXPECT_TRUE(fake_synced_session_client_ash()->is_session_sync_enabled());
}