chromium/chrome/browser/ash/app_mode/auto_sleep/device_weekly_scheduled_suspend_controller_browsertest.cc

// 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/ash/app_mode/auto_sleep/device_weekly_scheduled_suspend_controller.h"

#include <cstddef>
#include <functional>
#include <memory>
#include <utility>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/test/scoped_mock_time_message_loop_task_runner.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/ash/app_mode/auto_sleep/device_weekly_scheduled_suspend_test_policy_builder.h"
#include "chrome/browser/ash/app_mode/auto_sleep/fake_repeating_time_interval_task_executor.h"
#include "chrome/browser/ash/app_mode/kiosk_controller.h"
#include "chrome/browser/ash/app_mode/kiosk_system_session.h"
#include "chrome/browser/ash/login/app_mode/test/web_kiosk_base_test.h"
#include "chrome/browser/browser_process.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chromeos/ash/components/policy/weekly_time/weekly_time.h"
#include "chromeos/dbus/power/fake_power_manager_client.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {

namespace {

using DayOfWeek = DeviceWeeklyScheduledSuspendTestPolicyBuilder::DayOfWeek;

base::TimeDelta GetDuration(const base::Time& start,
                            const policy::WeeklyTime& end) {
  policy::WeeklyTime start_weekly_time =
      policy::WeeklyTime::GetLocalWeeklyTime(start);
  return start_weekly_time.GetDurationTo(end);
}

// Scoped test helper that is responsible for the following things:
// - Mocking time by setting up the dependencies on construction and cleaning
// them up to prevent dangling pointers.
// - Providing utility methods that allow controlling the time.
//
// This should be created inside the actual test so that we do not get
// stuck because of RunLoops within the browser test initialization code.
//
// Note: This is a scoped mock time class because it overrides the current task
// runner with a mock time task runner. As a consequence of this you have to
// drive the task runner yourself. This is possible to do via using the
// `FastForwardTimeTo` and the `task_runner()` methods in the class.
class ScopedMockTimeScheduledSuspendTestHelper {
 public:
  ScopedMockTimeScheduledSuspendTestHelper() {
    DeviceWeeklyScheduledSuspendController* controller =
        KioskController::Get()
            .GetKioskSystemSession()
            ->device_weekly_scheduled_suspend_controller_for_testing();

    controller->SetTaskExecutorFactoryForTesting(
        std::make_unique<FakeRepeatingTimeIntervalTaskExecutor::Factory>(
            task_runner_->GetMockClock(), task_runner_->GetMockTickClock()));
    controller->SetClockForTesting(task_runner_->GetMockClock());
  }

  ~ScopedMockTimeScheduledSuspendTestHelper() {
    DeviceWeeklyScheduledSuspendController* controller =
        KioskController::Get()
            .GetKioskSystemSession()
            ->device_weekly_scheduled_suspend_controller_for_testing();

    controller->SetTaskExecutorFactoryForTesting(nullptr);
    controller->SetClockForTesting(nullptr);
    // Clear the policy so that we don't get dangling task executors.
    g_browser_process->local_state()->SetList(
        prefs::kDeviceWeeklyScheduledSuspend, {});
  }

  void FastForwardTimeTo(const policy::WeeklyTime& weekly_time) {
    auto current_time = task_runner_->GetMockClock()->Now();
    auto duration = GetDuration(current_time, weekly_time);
    task_runner_->FastForwardBy(duration);
  }

  base::TestMockTimeTaskRunner* task_runner() {
    return task_runner_.task_runner();
  }

 private:
  base::ScopedMockTimeMessageLoopTaskRunner task_runner_;
};

}  // namespace

class DeviceWeeklyScheduledSuspendControllerTest : public WebKioskBaseTest {
 public:
  // WebKioskBaseTest:
  void SetUpOnMainThread() override {
    WebKioskBaseTest::SetUpOnMainThread();
    chromeos::FakePowerManagerClient::Get()->set_user_activity_callback(
        base::BindRepeating([](int& count) { ++count; },
                            std::ref(user_activity_calls_)));

    InitializeRegularOnlineKiosk();
    ASSERT_TRUE(KioskController::Get().GetKioskSystemSession());
  }

  void TearDownOnMainThread() override {
    chromeos::FakePowerManagerClient::Get()->set_user_activity_callback(
        base::NullCallback());
    WebKioskBaseTest::TearDownOnMainThread();
  }

  // Updates the policy preferences which in turn trigger the pref observers in
  // the controller.
  void UpdatePolicyPref(base::Value::List schedule_list) {
    g_browser_process->local_state()->SetList(
        prefs::kDeviceWeeklyScheduledSuspend, std::move(schedule_list));
  }

  void SimulateResumeSuspend() {
    chromeos::FakePowerManagerClient::Get()->SendDarkSuspendImminent();
  }

  int user_activity_calls() { return user_activity_calls_; }

 private:
  int user_activity_calls_ = 0;
};

IN_PROC_BROWSER_TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
                       SuspendControllerExistOnKioskStartUp) {
  ASSERT_TRUE(KioskController::Get()
                  .GetKioskSystemSession()
                  ->device_weekly_scheduled_suspend_controller_for_testing());
}

IN_PROC_BROWSER_TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
                       SuspendAndWakeTest) {
  ScopedMockTimeScheduledSuspendTestHelper helper;

  auto* power_client = chromeos::FakePowerManagerClient::Get();
  EXPECT_EQ(power_client->num_request_suspend_calls(), 0);
  EXPECT_EQ(user_activity_calls(), 0);
  auto policy_builder =
      DeviceWeeklyScheduledSuspendTestPolicyBuilder().AddWeeklySuspendInterval(
          DayOfWeek::MONDAY, base::Hours(0), DayOfWeek::MONDAY, base::Hours(9));
  auto intervals = policy_builder.GetAsWeeklyTimeIntervals();

  auto* clock = helper.task_runner()->GetMockClock();
  auto duration =
      GetDuration(clock->Now(), intervals[0]->start()) - base::Minutes(5);
  helper.task_runner()->FastForwardBy(duration);

  UpdatePolicyPref(policy_builder.GetAsPrefValue());

  EXPECT_EQ(power_client->num_request_suspend_calls(), 0);
  EXPECT_EQ(user_activity_calls(), 0);

  helper.FastForwardTimeTo(intervals[0]->start());
  EXPECT_EQ(power_client->num_request_suspend_calls(), 1);

  helper.FastForwardTimeTo(intervals[0]->end());

  SimulateResumeSuspend();

  EXPECT_EQ(user_activity_calls(), 1);
}

IN_PROC_BROWSER_TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
                       MultipleSuspendAndWakeTest) {
  ScopedMockTimeScheduledSuspendTestHelper helper;

  auto* power_client = chromeos::FakePowerManagerClient::Get();
  EXPECT_EQ(power_client->num_request_suspend_calls(), 0);
  EXPECT_EQ(user_activity_calls(), 0);
  auto builder =
      DeviceWeeklyScheduledSuspendTestPolicyBuilder()
          .AddWeeklySuspendInterval(DayOfWeek::MONDAY, base::Hours(0),
                                    DayOfWeek::MONDAY, base::Hours(9))
          .AddWeeklySuspendInterval(DayOfWeek::TUESDAY, base::Hours(0),
                                    DayOfWeek::TUESDAY, base::Hours(9))
          .AddWeeklySuspendInterval(DayOfWeek::WEDNESDAY, base::Hours(0),
                                    DayOfWeek::WEDNESDAY, base::Hours(9));
  auto intervals = builder.GetAsWeeklyTimeIntervals();

  auto* clock = helper.task_runner()->GetMockClock();
  auto duration =
      GetDuration(clock->Now(), intervals[0]->start()) - base::Minutes(5);
  helper.task_runner()->FastForwardBy(duration);

  EXPECT_EQ(power_client->num_request_suspend_calls(), 0);
  UpdatePolicyPref(builder.GetAsPrefValue());

  helper.FastForwardTimeTo(intervals[0]->start());
  EXPECT_EQ(power_client->num_request_suspend_calls(), 1);

  helper.FastForwardTimeTo(intervals[0]->end());
  SimulateResumeSuspend();
  EXPECT_EQ(user_activity_calls(), 1);

  helper.FastForwardTimeTo(intervals[1]->start());
  EXPECT_EQ(power_client->num_request_suspend_calls(), 2);

  helper.FastForwardTimeTo(intervals[1]->end());
  SimulateResumeSuspend();
  EXPECT_EQ(user_activity_calls(), 2);

  helper.FastForwardTimeTo(intervals[2]->start());
  EXPECT_EQ(power_client->num_request_suspend_calls(), 3);

  helper.FastForwardTimeTo(intervals[2]->end());
  SimulateResumeSuspend();
  EXPECT_EQ(user_activity_calls(), 3);

  EXPECT_EQ(power_client->num_request_suspend_calls(),
            static_cast<int>(intervals.size()));
  EXPECT_EQ(user_activity_calls(), static_cast<int>(intervals.size()));
}

IN_PROC_BROWSER_TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
                       ManualWakeDuringIntervalClearsState) {
  ScopedMockTimeScheduledSuspendTestHelper helper;

  auto* power_client = chromeos::FakePowerManagerClient::Get();
  auto policy_builder =
      DeviceWeeklyScheduledSuspendTestPolicyBuilder().AddWeeklySuspendInterval(
          DayOfWeek::TUESDAY, base::Hours(0), DayOfWeek::TUESDAY,
          base::Hours(9));
  auto intervals = policy_builder.GetAsWeeklyTimeIntervals();

  auto* clock = helper.task_runner()->GetMockClock();
  auto duration =
      GetDuration(clock->Now(), intervals[0]->start()) - base::Minutes(5);
  helper.task_runner()->FastForwardBy(duration);

  UpdatePolicyPref(policy_builder.GetAsPrefValue());

  EXPECT_EQ(power_client->num_request_suspend_calls(), 0);

  duration =
      GetDuration(clock->Now(), intervals[0]->start()) + base::Minutes(5);
  helper.task_runner()->FastForwardBy(duration);

  EXPECT_EQ(power_client->num_request_suspend_calls(), 1);

  // Simulate user waking up the device during the suspend by calling
  // `SendSuspendDone`.
  power_client->SendSuspendDone();

  helper.FastForwardTimeTo(intervals[0]->end());

  // Confirm that subsequent resume events will not cause
  // unnecessary user activity calls to wake the device when we are at the end
  // of the interval.
  SimulateResumeSuspend();

  EXPECT_EQ(user_activity_calls(), 0);
}

IN_PROC_BROWSER_TEST_F(DeviceWeeklyScheduledSuspendControllerTest,
                       SuspendAndWakeRepeatsEveryWeekTest) {
  ScopedMockTimeScheduledSuspendTestHelper helper;

  auto* power_client = chromeos::FakePowerManagerClient::Get();
  EXPECT_EQ(power_client->num_request_suspend_calls(), 0);
  EXPECT_EQ(user_activity_calls(), 0);
  auto policy_builder =
      DeviceWeeklyScheduledSuspendTestPolicyBuilder().AddWeeklySuspendInterval(
          DayOfWeek::SATURDAY, base::Hours(0), DayOfWeek::SATURDAY,
          base::Hours(9));
  auto intervals = policy_builder.GetAsWeeklyTimeIntervals();

  auto* clock = helper.task_runner()->GetMockClock();
  auto duration =
      GetDuration(clock->Now(), intervals[0]->start()) - base::Minutes(5);
  helper.task_runner()->FastForwardBy(duration);

  UpdatePolicyPref(policy_builder.GetAsPrefValue());

  EXPECT_EQ(power_client->num_request_suspend_calls(), 0);
  EXPECT_EQ(user_activity_calls(), 0);

  helper.FastForwardTimeTo(intervals[0]->start());
  EXPECT_EQ(power_client->num_request_suspend_calls(), 1);

  // Resume the device before the end of the interval.
  power_client->SendSuspendDone();

  // Confirm that the device calls request suspend again in a week.
  helper.task_runner()->FastForwardBy(base::Days(7));
  EXPECT_EQ(power_client->num_request_suspend_calls(), 2);
}

}  // namespace ash