| // 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); |
| } |