chromium/chrome/browser/ash/policy/remote_commands/device_command_reboot_job_unittest.cc

// 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/ash/policy/remote_commands/device_command_reboot_job.h"

#include <memory>

#include "ash/constants/ash_pref_names.h"
#include "ash/constants/ash_switches.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_command_line.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chrome/browser/ash/policy/remote_commands/device_command_reboot_job_test_util.h"
#include "chromeos/dbus/power/fake_power_manager_client.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace em = enterprise_management;

namespace policy {

namespace {

constexpr base::TimeDelta kCommandAge = base::Minutes(5);
constexpr base::TimeDelta kAlmostExpiredCommandAge = base::Minutes(10);

constexpr base::TimeDelta kUserSessionRebootDelay = base::Minutes(3);
constexpr base::TimeDelta kDefaultUserSessionRebootDelay = base::Minutes(5);

}  // namespace

class DeviceCommandRebootJobTest : public DeviceCommandRebootJobTestBase,
                                   public testing::Test {
 protected:
  std::unique_ptr<DeviceCommandRebootJob> CreateAndInitializeCommand(
      base::TimeDelta age_of_command) {
    return DeviceCommandRebootJobTestBase::CreateAndInitializeCommand(
        age_of_command, kUserSessionRebootDelay);
  }

  std::unique_ptr<DeviceCommandRebootJob> CreateAndInitializeCommand() {
    return DeviceCommandRebootJobTestBase::CreateAndInitializeCommand(
        /*age_of_command=*/base::TimeDelta(), kUserSessionRebootDelay);
  }
};

// Test that the command expires after default expiration time.
TEST_F(DeviceCommandRebootJobTest, ExpiresAfterExpirationTime) {
  auto scoped_login_state = ScopedLoginState::CreateKiosk();

  auto old_command = CreateAndInitializeCommand();
  task_environment_.FastForwardBy(kAlmostExpiredCommandAge + base::Seconds(1));

  EXPECT_FALSE(old_command->Run(Now(), NowTicks(),
                                RemoteCommandJob::FinishedCallback()));
  EXPECT_EQ(old_command->status(), RemoteCommandJob::EXPIRED);
}

// Test that the command does not expire before default expiration time.
TEST_F(DeviceCommandRebootJobTest, DoesNotExpireBeforeExpirationTime) {
  auto scoped_login_state = ScopedLoginState::CreateKiosk();

  auto fresh_command = CreateAndInitializeCommand();
  task_environment_.FastForwardBy(kAlmostExpiredCommandAge);

  EXPECT_TRUE(fresh_command->Run(Now(), NowTicks(),
                                 RemoteCommandJob::FinishedCallback()));
}

// Test that the command does not trigger reboot if boot happened after the
// command was issued
TEST_F(DeviceCommandRebootJobTest, DoesNotRebootIfBootedRecently) {
  auto scoped_login_state = ScopedLoginState::CreateKiosk();

  // Boot time is considered as 0 time ticks. Initialize the command so it's
  // issued before boot time but has not expired yet.
  // The implementation relies on base::TimeTicks supporting negative values.
  //   -5min        0min                 1min
  // ---------------------------------------
  //   ^            ^                    ^
  //   |            |                    |
  //   issue_time   boot_time            now

  // Create the command with `kCommandAge` age before boot time.
  auto command = CreateAndInitializeCommand(kCommandAge);

  base::test::TestFuture<void> future;
  EXPECT_TRUE(command->Run(Now(), NowTicks(), future.GetCallback()));

  ASSERT_TRUE(future.Wait());
  EXPECT_EQ(command->status(), RemoteCommandJob::SUCCEEDED);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 0);
}

// Test that the command instantly reboots the device in a kiosk mode.
TEST_F(DeviceCommandRebootJobTest, RebootsKioskInstantly) {
  auto scoped_login_state = ScopedLoginState::CreateKiosk();

  auto command = CreateAndInitializeCommand();
  base::test::TestFuture<void> future;
  command->Run(Now(), NowTicks(), future.GetCallback());

  ASSERT_TRUE(future.Wait());
  EXPECT_EQ(command->status(), RemoteCommandJob::SUCCEEDED);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 1);
}

// Test that the command instantly reboots the device outside of a user session
// on the login screen.
TEST_F(DeviceCommandRebootJobTest, RebootsInstantlyOutsideOfSession) {
  auto scoped_login_state = ScopedLoginState::CreateLoggedOut();

  auto command = CreateAndInitializeCommand();
  base::test::TestFuture<void> future;
  command->Run(Now(), NowTicks(), future.GetCallback());

  ASSERT_TRUE(future.Wait());
  EXPECT_EQ(command->status(), RemoteCommandJob::SUCCEEDED);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 1);
}

// Tests that in the user session, the command does not instantly reboots util a
// user logs out.
TEST_F(DeviceCommandRebootJobTest, RebootsOnUserLogout) {
  auto scoped_login_state = ScopedLoginState::CreateRegularUser();

  auto command = CreateAndInitializeCommand();
  base::test::TestFuture<void> future;
  command->Run(Now(), NowTicks(), future.GetCallback());

  // Fast forward time a little but before command timeout expires. Check that
  // reboot has not happened yet.
  task_environment_.FastForwardBy(kUserSessionRebootDelay / 2);

  EXPECT_EQ(command->status(), RemoteCommandJob::RUNNING);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 0);

  session_termination_manager_.StopSession(
      login_manager::SessionStopReason::USER_REQUESTS_SIGNOUT);

  ASSERT_TRUE(future.Wait());
  EXPECT_EQ(command->status(), RemoteCommandJob::SUCCEEDED);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 1);
  EXPECT_FALSE(prefs_->GetBoolean(ash::prefs::kShowPostRebootNotification));
}

// Tests that in the user session, the command shows the notification, waits for
// timeout and only then reboots.
TEST_F(DeviceCommandRebootJobTest,
       ShowsNotificationAndRebootsAfterTimeoutInSession) {
  auto scoped_login_state = ScopedLoginState::CreateRegularUser();

  auto command = CreateAndInitializeCommand();
  base::test::TestFuture<void> future;
  command->Run(Now(), NowTicks(), future.GetCallback());

  // Check that reboot notification is shown after timer starts. Fast forward
  // without an actual time so that the timer task is triggered.
  task_environment_.FastForwardBy(base::TimeDelta());
  EXPECT_EQ(fake_notifications_scheduler_->GetShowNotificationCalls(), 1);
  EXPECT_EQ(fake_notifications_scheduler_->GetShowDialogCalls(), 1);

  // Fast forward time a little but before command timeout expires. Check that
  // reboot has not happened yet.
  task_environment_.FastForwardBy(kUserSessionRebootDelay / 2);

  // Before being shown for the first time, the notification scheduler closes
  // itself. That's why we expect 1 instead of 0.
  EXPECT_EQ(fake_notifications_scheduler_->GetCloseNotificationCalls(), 1);

  // Check that the command is still running as timeout is not reached yet.
  EXPECT_EQ(command->status(), RemoteCommandJob::RUNNING);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 0);

  // Finish fastforwarding the timeout.
  task_environment_.FastForwardBy(kUserSessionRebootDelay / 2);
  ASSERT_TRUE(future.Wait());
  EXPECT_EQ(command->status(), RemoteCommandJob::SUCCEEDED);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 1);
  EXPECT_TRUE(prefs_->GetBoolean(ash::prefs::kShowPostRebootNotification));
  EXPECT_EQ(fake_notifications_scheduler_->GetCloseNotificationCalls(), 2);
}

// Tests that in the user session, the command shows the notification and
// reboots when a user clicks the reboot button.
TEST_F(DeviceCommandRebootJobTest,
       ShowsNotificationAndRebootsWhenUserClicksRestartButtonInSession) {
  auto scoped_login_state = ScopedLoginState::CreateRegularUser();

  auto command = CreateAndInitializeCommand();
  base::test::TestFuture<void> future;
  command->Run(Now(), NowTicks(), future.GetCallback());

  // Check that reboot notification is shown after timer starts. Fast forward
  // without an actual time so that the timer task is triggered.
  task_environment_.FastForwardBy(base::TimeDelta());
  EXPECT_EQ(fake_notifications_scheduler_->GetShowNotificationCalls(), 1);
  EXPECT_EQ(fake_notifications_scheduler_->GetShowDialogCalls(), 1);

  // Fast forward time a little but before command timeout expires. Check that
  // reboot has not happened yet.
  task_environment_.FastForwardBy(kUserSessionRebootDelay / 2);

  // Before being shown for the first time, the notification scheduler closes
  // itself. That's why we expect 1 instead of 0.
  EXPECT_EQ(fake_notifications_scheduler_->GetCloseNotificationCalls(), 1);

  // Check that the command is still running as timeout is not reached yet.
  EXPECT_EQ(command->status(), RemoteCommandJob::RUNNING);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 0);

  // Simulate a user clicking the reboot button on dialog and check the reboot
  // is immediately requested.
  fake_notifications_scheduler_->SimulateRebootButtonClick();

  ASSERT_TRUE(future.Wait());
  EXPECT_EQ(command->status(), RemoteCommandJob::SUCCEEDED);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 1);
  EXPECT_FALSE(prefs_->GetBoolean(ash::prefs::kShowPostRebootNotification));
  EXPECT_EQ(fake_notifications_scheduler_->GetCloseNotificationCalls(), 2);
}

// Tests that the command delays execution until power manager availability is
// known, and reboots once power manager is available.
TEST_F(DeviceCommandRebootJobTest, RebootsWhenPowerManagerIsAvailable) {
  auto scoped_login_state = ScopedLoginState::CreateKiosk();

  chromeos::FakePowerManagerClient::Get()->SetServiceAvailability(
      /*availability=*/std::nullopt);

  auto command = CreateAndInitializeCommand();
  base::test::TestFuture<void> future;
  command->Run(Now(), NowTicks(), future.GetCallback());

  // Check that command is still running while power manager availability is
  // unknown.
  task_environment_.FastForwardBy(base::TimeDelta());
  EXPECT_EQ(command->status(), RemoteCommandJob::RUNNING);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 0);

  chromeos::FakePowerManagerClient::Get()->SetServiceAvailability(
      /*availability=*/true);

  ASSERT_TRUE(future.Wait());
  EXPECT_EQ(command->status(), RemoteCommandJob::SUCCEEDED);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 1);
}

// Tests that the command fails when power manager is not initialized.
TEST_F(DeviceCommandRebootJobTest,
       ReportsFailureWhenPowerManagerIsNotInitialized) {
  auto scoped_login_state = ScopedLoginState::CreateKiosk();

  chromeos::FakePowerManagerClient::Get()->Shutdown();
  EXPECT_FALSE(chromeos::PowerManagerClient::Get());

  auto command = CreateAndInitializeCommand();
  base::test::TestFuture<void> future;
  command->Run(Now(), NowTicks(), future.GetCallback());

  ASSERT_TRUE(future.Wait());
  EXPECT_EQ(command->status(), RemoteCommandJob::FAILED);
}

// Tests that the command fails when power manager is unavailable.
TEST_F(DeviceCommandRebootJobTest,
       ReportsFailureWhenPowerManagerIsUnavailable) {
  auto scoped_login_state = ScopedLoginState::CreateKiosk();

  chromeos::FakePowerManagerClient::Get()->SetServiceAvailability(
      /*availability=*/false);

  auto command = CreateAndInitializeCommand();
  base::test::TestFuture<void> future;
  command->Run(Now(), NowTicks(), future.GetCallback());

  ASSERT_TRUE(future.Wait());
  EXPECT_EQ(command->status(), RemoteCommandJob::FAILED);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 0);
}

class DeviceCommandRebootJobPayloadTest : public DeviceCommandRebootJobTestBase,
                                          public testing::Test {
 private:
  ScopedLoginState scoped_login_state_{ScopedLoginState::CreateRegularUser()};
};

TEST_F(DeviceCommandRebootJobPayloadTest, RebootsAfterPayloadDelay) {
  constexpr base::TimeDelta kUserSessionPayloadDelay = base::Minutes(10);
  auto command = CreateAndInitializeCommand(
      /*age_of_command=*/base::TimeDelta(), kUserSessionPayloadDelay);
  base::test::TestFuture<void> future;
  command->Run(Now(), NowTicks(), future.GetCallback());

  // Check the command does not reboot up until timeout.
  task_environment_.FastForwardBy(kUserSessionPayloadDelay - base::Seconds(1));
  EXPECT_EQ(command->status(), RemoteCommandJob::RUNNING);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 0);

  task_environment_.FastForwardBy(base::Seconds(1));

  ASSERT_TRUE(future.Wait());
  EXPECT_EQ(command->status(), RemoteCommandJob::SUCCEEDED);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 1);
}

TEST_F(DeviceCommandRebootJobPayloadTest, RebootsInstantlyWithZeroDelay) {
  auto command = CreateAndInitializeCommand(
      /*age_of_command=*/base::TimeDelta(),
      /*user_session_reboot_delay=*/base::TimeDelta());
  base::test::TestFuture<void> future;
  command->Run(Now(), NowTicks(), future.GetCallback());

  task_environment_.FastForwardBy(base::TimeDelta());

  ASSERT_TRUE(future.Wait());
  EXPECT_EQ(command->status(), RemoteCommandJob::SUCCEEDED);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 1);

  // Check that notification is not shown but postreboot notification is
  // scheduled.
  EXPECT_EQ(fake_notifications_scheduler_->GetShowNotificationCalls(), 0);
  EXPECT_EQ(fake_notifications_scheduler_->GetShowDialogCalls(), 0);
  EXPECT_TRUE(prefs_->GetBoolean(ash::prefs::kShowPostRebootNotification));
  EXPECT_EQ(fake_notifications_scheduler_->GetCloseNotificationCalls(), 0);
}

TEST_F(DeviceCommandRebootJobPayloadTest, RebootsAfterCommandLineDelay) {
  constexpr base::TimeDelta user_session_reboot_delay = base::Minutes(20);

  base::test::ScopedCommandLine scoped_command_line;
  scoped_command_line.GetProcessCommandLine()->AppendSwitchASCII(
      ash::switches::kRemoteRebootCommandDelayInSecondsForTesting,
      base::NumberToString(user_session_reboot_delay.InSeconds()));
  // Use smaller delay in payload so we can check that reboot happens only
  // after commandline delay.
  auto command = CreateAndInitializeCommand(
      /*age_of_command=*/base::TimeDelta(), user_session_reboot_delay / 2);
  base::test::TestFuture<void> future;
  command->Run(Now(), NowTicks(), future.GetCallback());

  // Check that reboot does not happen after payload delay and before
  // commandline delay.
  task_environment_.FastForwardBy(user_session_reboot_delay / 2);
  EXPECT_EQ(command->status(), RemoteCommandJob::RUNNING);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 0);

  task_environment_.FastForwardBy(user_session_reboot_delay / 2);

  ASSERT_TRUE(future.Wait());
  EXPECT_EQ(command->status(), RemoteCommandJob::SUCCEEDED);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 1);
}

TEST_F(DeviceCommandRebootJobPayloadTest, RebootsAfterDefaultDelay) {
  em::RemoteCommand command_proto;
  command_proto.set_type(em::RemoteCommand_Type_DEVICE_REBOOT);
  command_proto.set_command_id(123);
  command_proto.set_age_of_command(0);
  ASSERT_FALSE(command_proto.has_payload());

  auto command = CreateAndInitializeCommand(std::move(command_proto));

  base::test::TestFuture<void> future;
  command->Run(Now(), NowTicks(), future.GetCallback());

  // Check that reboot does not happen immediately after the command is
  // received.
  task_environment_.FastForwardBy(base::TimeDelta());
  EXPECT_EQ(command->status(), RemoteCommandJob::RUNNING);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 0);

  task_environment_.FastForwardBy(kDefaultUserSessionRebootDelay);

  ASSERT_TRUE(future.Wait());
  EXPECT_EQ(command->status(), RemoteCommandJob::SUCCEEDED);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 1);
}

class DeviceCommandRebootJobInvalidPayloadTest
    : public DeviceCommandRebootJobPayloadTest,
      public testing::WithParamInterface<std::string> {
 public:
  std::string payload() const { return GetParam(); }
};

TEST_P(DeviceCommandRebootJobInvalidPayloadTest, RebootsWithInvalidPayload) {
  em::RemoteCommand command_proto;
  command_proto.set_type(em::RemoteCommand_Type_DEVICE_REBOOT);
  command_proto.set_command_id(123);
  command_proto.set_age_of_command(0);
  command_proto.set_payload(std::string(payload()));

  auto command = CreateAndInitializeCommand(std::move(command_proto));

  base::test::TestFuture<void> future;
  command->Run(Now(), NowTicks(), future.GetCallback());

  // Check that reboot does not happen immediately after the command is
  // received.
  task_environment_.FastForwardBy(base::TimeDelta());
  EXPECT_EQ(command->status(), RemoteCommandJob::RUNNING);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 0);

  task_environment_.FastForwardBy(kDefaultUserSessionRebootDelay);

  ASSERT_TRUE(future.Wait());
  EXPECT_EQ(command->status(), RemoteCommandJob::SUCCEEDED);
  EXPECT_EQ(
      chromeos::FakePowerManagerClient::Get()->num_request_restart_calls(), 1);
}

INSTANTIATE_TEST_SUITE_P(
    InvalidPayloads,
    DeviceCommandRebootJobInvalidPayloadTest,
    testing::Values("not_a_json",
                    "{}",
                    R"({"not_delay_field": 10})",
                    R"({"user_session_delay_seconds": false})",
                    R"({"user_session_delay_seconds": -1})",
                    R"({"user_session_delay_seconds": 1.0})",
                    R"({"user_session_delay_seconds": 1.10})",
                    R"({"user_session_delay_seconds": -1.0})",
                    R"({"user_session_delay_seconds": -1.10})",
                    base::StringPrintf(R"({"user_session_delay_seconds": %ld})",
                                       base::TimeDelta::Max().InSeconds()),
                    base::StringPrintf(R"({"user_session_delay_seconds": %ld})",
                                       base::TimeDelta::Min().InSeconds())));

}  // namespace policy