blob: c691bed40a42244392d5bca4fe31e0d06ea00b44 [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 "chrome/browser/user_education/recent_session_tracker.h"
#include <memory>
#include <optional>
#include <vector>
#include "base/callback_list.h"
#include "base/memory/raw_ref.h"
#include "base/test/bind.h"
#include "base/test/simple_test_clock.h"
#include "base/time/time.h"
#include "chrome/browser/user_education/browser_user_education_storage_service.h"
#include "components/user_education/common/session/user_education_session_manager.h"
#include "components/user_education/common/user_education_data.h"
#include "components/user_education/common/user_education_storage_service.h"
#include "components/user_education/test/test_user_education_storage_service.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
// At some point these may need to be moved to a common header file.
// Short-circuits all of the functionality of the session manager except for
// updating session data and sending notifications.
class FakeSessionManager
: protected user_education::UserEducationSessionManager {
public:
explicit FakeSessionManager(
user_education::UserEducationStorageService& storage_service)
: storage_service_(storage_service) {}
~FakeSessionManager() override = default;
// Simulates a new session at the current time provided by the storage
// service's clock.
void SimulateNewSession() {
const auto old_session = storage_service_->ReadSessionData();
const auto now = storage_service_->GetCurrentTime();
user_education::UserEducationSessionData new_session =
storage_service_->ReadSessionData();
new_session.most_recent_active_time = now;
new_session.start_time = now;
++new_session.session_number;
storage_service_->SaveSessionData(new_session);
OnNewSession(old_session.start_time, old_session.most_recent_active_time,
now);
}
user_education::UserEducationSessionManager& AsSessionManager() {
return *static_cast<user_education::UserEducationSessionManager*>(this);
}
private:
const raw_ref<user_education::UserEducationStorageService> storage_service_;
};
// Implementation of `RecentSessionDataStorageService` that extends
// `TestUserEducationStorageService` with in-memory data storage.
class TestBrowserStorageService
: public user_education::test::TestUserEducationStorageService,
public RecentSessionDataStorageService {
public:
TestBrowserStorageService() = default;
~TestBrowserStorageService() override = default;
RecentSessionData ReadRecentSessionData() const override {
RecentSessionData data;
data.recent_session_start_times = recent_sessions_;
data.enabled_time = enabled_time_;
return data;
}
void SaveRecentSessionData(const RecentSessionData& data) override {
recent_sessions_ = data.recent_session_start_times;
enabled_time_ = data.enabled_time;
}
private:
std::vector<base::Time> recent_sessions_;
std::optional<base::Time> enabled_time_;
};
base::Time Days(double d) {
return base::Time::FromSecondsSinceUnixEpoch(d * 24 * 60 * 60);
}
} // namespace
class RecentSessionTrackerTest : public testing::Test {
public:
RecentSessionTrackerTest() {
storage_service_.set_clock_for_testing(&clock_);
}
~RecentSessionTrackerTest() override = default;
// Sets up the data for the test.
// - saves a list of `old_sessions` to the storage service
// - optionally starts a new session in the current process
// - creates the tracker and initializes it
//
// Use `session_before_tracker_init` if you want to simulate a session
// starting before the tracker is created; otherwise you can call
// `StartNewSession()` to create a new session later.
void Init(const std::vector<base::Time>& old_sessions,
std::optional<base::Time> session_before_tracker_init) {
RecentSessionData data;
data.recent_session_start_times = old_sessions;
if (!old_sessions.empty()) {
data.enabled_time = old_sessions.back();
}
storage_service_.SaveRecentSessionData(data);
if (session_before_tracker_init) {
StartNewSession(*session_before_tracker_init);
}
tracker_ = std::make_unique<RecentSessionTracker>(
session_manager_.AsSessionManager(), storage_service_,
storage_service_);
update_subscription_ = tracker_->AddRecentSessionsUpdatedCallback(
base::BindLambdaForTesting([this](const RecentSessionData& data) {
++update_count_;
last_update_data_ = data;
}));
}
// Starts a new session at `session_time`.
void StartNewSession(base::Time session_time) {
clock_.SetNow(session_time);
session_manager_.SimulateNewSession();
}
void EnsureRecentSessions(const std::vector<base::Time>& expected_data,
int expected_update_count) const {
const auto recent_sessions = storage_service_.ReadRecentSessionData();
EXPECT_THAT(recent_sessions.recent_session_start_times,
testing::ContainerEq(expected_data));
EXPECT_EQ(expected_update_count, update_count_);
if (last_update_data_) {
EXPECT_THAT(last_update_data_->recent_session_start_times,
testing::ContainerEq(expected_data));
}
EXPECT_EQ(expected_update_count > 0,
tracker_->recent_session_data_for_testing().has_value());
if (tracker_->recent_session_data_for_testing().has_value()) {
EXPECT_THAT(tracker_->recent_session_data_for_testing()
->recent_session_start_times,
testing::ContainerEq(expected_data));
}
ASSERT_EQ(!recent_sessions.recent_session_start_times.empty(),
recent_sessions.enabled_time.has_value());
if (recent_sessions.enabled_time) {
EXPECT_LE(*recent_sessions.enabled_time,
recent_sessions.recent_session_start_times.back());
}
}
private:
base::SimpleTestClock clock_;
TestBrowserStorageService storage_service_;
FakeSessionManager session_manager_{storage_service_};
std::unique_ptr<RecentSessionTracker> tracker_;
int update_count_ = 0;
std::optional<RecentSessionData> last_update_data_;
base::CallbackListSubscription update_subscription_;
};
TEST_F(RecentSessionTrackerTest, NoNewSession) {
const std::vector<base::Time> old_sessions{
Days(102),
Days(101),
Days(100),
};
Init(old_sessions, std::nullopt);
EnsureRecentSessions(old_sessions, 0);
}
TEST_F(RecentSessionTrackerTest, NewSessionBeforeInit) {
std::vector<base::Time> sessions{
Days(102),
Days(101),
Days(100),
};
Init(sessions, Days(103));
sessions.insert(sessions.begin(), Days(103));
EnsureRecentSessions(sessions, 1);
}
TEST_F(RecentSessionTrackerTest, NewSessionAfterInit) {
std::vector<base::Time> sessions{
Days(102),
Days(101),
Days(100),
};
Init(sessions, std::nullopt);
StartNewSession(Days(103));
sessions.insert(sessions.begin(), Days(103));
EnsureRecentSessions(sessions, 1);
}
TEST_F(RecentSessionTrackerTest, OldSessionsRollOff) {
std::vector<base::Time> sessions{
Days(103),
Days(102),
Days(101),
Days(100),
};
// Move forward enough that the first two entries roll out of the window.
const base::Time time1 = Days(102) +
RecentSessionTracker::kMaxRecentSessionRetention -
base::Seconds(1);
Init(sessions, time1);
sessions = {
time1,
Days(103),
Days(102),
};
EnsureRecentSessions(sessions, 1);
// Move forward enough to cause the next entry to roll off.
const base::Time time2 = time1 + base::Seconds(2);
StartNewSession(time2);
sessions = {
time2,
time1,
Days(103),
};
EnsureRecentSessions(sessions, 2);
// Move forward enough to cause all entries but the current one to roll off.
const base::Time time3 = time2 +
RecentSessionTracker::kMaxRecentSessionRetention +
base::Seconds(1);
StartNewSession(time3);
sessions = {time3};
EnsureRecentSessions(sessions, 3);
}
TEST_F(RecentSessionTrackerTest, TooManySessionsRollOff) {
// Create a list of sessions of max length.
std::vector<base::Time> sessions;
for (int i = RecentSessionTracker::kMaxRecentSessionRecords; i > 0; i--) {
sessions.push_back(Days(100 + i));
}
// Add a new session; the last session should roll off.
const base::Time time1 = sessions.front() + base::Days(1);
Init(sessions, time1);
sessions.pop_back();
sessions.insert(sessions.begin(), time1);
EnsureRecentSessions(sessions, 1);
// Add a new session; the last session should roll off.
const base::Time time2 = time1 + base::Days(1);
StartNewSession(time2);
sessions.pop_back();
sessions.insert(sessions.begin(), time2);
EnsureRecentSessions(sessions, 2);
}
TEST_F(RecentSessionTrackerTest, ClockSetBackElidesValues) {
std::vector<base::Time> sessions{
Days(104),
Days(103),
Days(101),
Days(100),
};
Init(sessions, Days(102));
sessions = {
Days(102),
Days(101),
Days(100),
};
EnsureRecentSessions(sessions, 1);
}
TEST_F(RecentSessionTrackerTest, EnabledForTheFirstTime) {
Init({}, std::nullopt);
EnsureRecentSessions({}, 0);
StartNewSession(Days(100));
EnsureRecentSessions({Days(100)}, 1);
}