| // 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 "chromeos/ash/components/policy/restriction_schedule/device_restriction_schedule_controller.h" | 
 |  | 
 | #include <utility> | 
 |  | 
 | #include "ash/constants/ash_switches.h" | 
 | #include "base/check_deref.h" | 
 | #include "base/functional/callback.h" | 
 | #include "base/json/json_string_value_serializer.h" | 
 | #include "base/location.h" | 
 | #include "base/scoped_observation.h" | 
 | #include "base/test/simple_test_clock.h" | 
 | #include "base/test/test_future.h" | 
 | #include "base/time/clock.h" | 
 | #include "base/time/default_clock.h" | 
 | #include "base/time/time.h" | 
 | #include "base/timer/wall_clock_timer.h" | 
 | #include "chrome/browser/ash/login/login_manager_test.h" | 
 | #include "chrome/browser/ash/login/test/device_state_mixin.h" | 
 | #include "chrome/browser/ash/login/test/js_checker.h" | 
 | #include "chrome/browser/ash/login/test/login_manager_mixin.h" | 
 | #include "chrome/browser/ash/login/test/oobe_screen_waiter.h" | 
 | #include "chrome/browser/ash/login/test/scoped_policy_update.h" | 
 | #include "chrome/browser/browser_process.h" | 
 | #include "chrome/browser/browser_process_platform_part.h" | 
 | #include "chrome/browser/lifetime/termination_notification.h" | 
 | #include "chrome/browser/ui/webui/ash/login/device_disabled_screen_handler.h" | 
 | #include "chromeos/ash/components/policy/restriction_schedule/device_restriction_schedule_controller_delegate_impl.h" | 
 | #include "chromeos/ash/components/policy/weekly_time/test_support.h" | 
 | #include "chromeos/strings/grit/chromeos_strings.h" | 
 | #include "components/policy/proto/chrome_device_policy.pb.h" | 
 | #include "content/public/test/browser_test.h" | 
 | #include "ui/base/l10n/l10n_util.h" | 
 | #include "ui/message_center/message_center.h" | 
 | #include "ui/message_center/message_center_observer.h" | 
 |  | 
 | namespace policy { | 
 |  | 
 | using ::ash::test::OobeJS; | 
 | using ::ash::test::UIPath; | 
 |  | 
 | // Empty list. | 
 | constexpr const char* kPolicyJsonEmpty = "[]"; | 
 |  | 
 | constexpr char kDeviceDisabledId[] = "device-disabled"; | 
 | constexpr UIPath kBannerTitle = {kDeviceDisabledId, "title"}; | 
 | constexpr UIPath kBannerContents = {kDeviceDisabledId, "subtitle"}; | 
 |  | 
 | class DeviceRestrictionScheduleControllerTest : public ash::LoginManagerTest { | 
 |  public: | 
 |   DeviceRestrictionScheduleControllerTest() { | 
 |     login_mixin_.AppendRegularUsers(1); | 
 |   } | 
 |  | 
 |   void SetUpCommandLine(base::CommandLine* command_line) override { | 
 |     LoginManagerTest::SetUpCommandLine(command_line); | 
 |     // Allow failing policy fetch so that we don't shutdown the profile on | 
 |     // failure. | 
 |     command_line->AppendSwitch(ash::switches::kAllowFailedPolicyFetchForTest); | 
 |   } | 
 |  | 
 |   void UpdatePolicy(const std::string& policy_str) { | 
 |     std::unique_ptr<ash::ScopedDevicePolicyUpdate> device_policy_update = | 
 |         device_state_.RequestDevicePolicyUpdate(); | 
 |     device_policy_update->policy_payload() | 
 |         ->mutable_devicerestrictionschedule() | 
 |         ->set_value(policy_str.c_str()); | 
 |   } | 
 |  | 
 |   void SetRestrictionSchedule(base::TimeDelta from_now, | 
 |                               base::TimeDelta duration) { | 
 |     base::Value::List policy_list = | 
 |         weekly_time::BuildList(clock_->Now(), from_now, duration); | 
 |     std::string policy_str; | 
 |     ASSERT_TRUE(JSONStringValueSerializer(&policy_str).Serialize(policy_list)); | 
 |     UpdatePolicy(policy_str); | 
 |   } | 
 |  | 
 |   DeviceRestrictionScheduleController& controller() { | 
 |     return CHECK_DEREF(g_browser_process->platform_part() | 
 |                            ->device_restriction_schedule_controller()); | 
 |   } | 
 |  | 
 |   void SetClocks(const base::Clock& clock) { | 
 |     clock_ = clock; | 
 |     controller().SetClockForTesting(clock); | 
 |   } | 
 |  | 
 |  protected: | 
 |   ash::DeviceStateMixin device_state_{ | 
 |       &mixin_host_, | 
 |       ash::DeviceStateMixin::State::OOBE_COMPLETED_CLOUD_ENROLLED}; | 
 |   ash::LoginManagerMixin login_mixin_{&mixin_host_}; | 
 |   raw_ref<const base::Clock> clock_{*base::DefaultClock::GetInstance()}; | 
 | }; | 
 |  | 
 | class CaptureNotificationWaiter : public message_center::MessageCenterObserver { | 
 |  public: | 
 |   CaptureNotificationWaiter(base::OnceClosure on_notification_added, | 
 |                             const std::string& match_notification_id) | 
 |       : on_notification_added_(std::move(on_notification_added)), | 
 |         match_notification_id_(match_notification_id) { | 
 |     auto* message_center = message_center::MessageCenter::Get(); | 
 |     if (message_center->FindNotificationById(match_notification_id_)) { | 
 |       std::move(on_notification_added_).Run(); | 
 |       return; | 
 |     } | 
 |     observation_.Observe(message_center); | 
 |   } | 
 |  | 
 |   // message_center::MessageCenterObserver: | 
 |   void OnNotificationAdded(const std::string& notification_id) override { | 
 |     if (notification_id == match_notification_id_) { | 
 |       std::move(on_notification_added_).Run(); | 
 |     } | 
 |   } | 
 |  | 
 |  private: | 
 |   base::OnceClosure on_notification_added_; | 
 |   std::string match_notification_id_; | 
 |   base::ScopedObservation<message_center::MessageCenter, | 
 |                           message_center::MessageCenterObserver> | 
 |       observation_{this}; | 
 | }; | 
 |  | 
 | class FakeWallClockTimer : public base::WallClockTimer { | 
 |  public: | 
 |   void Start(const base::Location& posted_from, | 
 |              base::Time desired_run_time, | 
 |              base::OnceClosure user_task) override { | 
 |     user_task_ = std::move(user_task); | 
 |   } | 
 |  | 
 |   void FireNow() { std::move(user_task_).Run(); } | 
 |  | 
 |  private: | 
 |   base::OnceClosure user_task_; | 
 | }; | 
 |  | 
 | IN_PROC_BROWSER_TEST_F(DeviceRestrictionScheduleControllerTest, | 
 |                        UpcomingLogoutNotificationShows) { | 
 |   LoginUser(login_mixin_.users()[0].account_id); | 
 |  | 
 |   // Restriction schedule starts in 20 minutes and lasts for 2 hours. | 
 |   SetRestrictionSchedule(base::Minutes(20), base::Hours(2)); | 
 |  | 
 |   // Verify that upcoming session end notification shows. | 
 |   base::test::TestFuture<void> future; | 
 |   CaptureNotificationWaiter waiter( | 
 |       future.GetCallback(), DeviceRestrictionScheduleControllerDelegateImpl:: | 
 |                                 kUpcomingLogoutNotificationId); | 
 |   ASSERT_TRUE(future.Wait()); | 
 | } | 
 |  | 
 | IN_PROC_BROWSER_TEST_F(DeviceRestrictionScheduleControllerTest, | 
 |                        PRE_PostLogoutNotificationShows) { | 
 |   LoginUser(login_mixin_.users()[0].account_id); | 
 |  | 
 |   // Restriction schedule started 20 minutes ago and lasts for 2 hours. | 
 |   SetRestrictionSchedule(-base::Minutes(20), base::Hours(2)); | 
 |  | 
 |   // Logout happens here (Chrome shuts down), and then we start again on the | 
 |   // login screen in the next part of the test. | 
 | } | 
 |  | 
 | IN_PROC_BROWSER_TEST_F(DeviceRestrictionScheduleControllerTest, | 
 |                        PostLogoutNotificationShows) { | 
 |   // Verify that post-logout notification shows. | 
 |   base::test::TestFuture<void> future; | 
 |   CaptureNotificationWaiter waiter( | 
 |       future.GetCallback(), DeviceRestrictionScheduleControllerDelegateImpl:: | 
 |                                 kPostLogoutNotificationId); | 
 |   ASSERT_TRUE(future.Wait()); | 
 | } | 
 |  | 
 | IN_PROC_BROWSER_TEST_F(DeviceRestrictionScheduleControllerTest, | 
 |                        LogoutOnEnteringRestrictedSchedule) { | 
 |   LoginUser(login_mixin_.users()[0].account_id); | 
 |  | 
 |   // Restriction schedule started 20 minutes ago and lasts for 2 hours. | 
 |   SetRestrictionSchedule(-base::Minutes(20), base::Hours(2)); | 
 |  | 
 |   // Verify that logout happens (Chrome shuts down) upon entering the restricted | 
 |   // schedule. | 
 |   base::test::TestFuture<void> future; | 
 |   auto subscription = | 
 |       browser_shutdown::AddAppTerminatingCallback(future.GetCallback()); | 
 |   ASSERT_TRUE(future.Wait()); | 
 | } | 
 |  | 
 | IN_PROC_BROWSER_TEST_F(DeviceRestrictionScheduleControllerTest, | 
 |                        DeviceDisabledScreenShows) { | 
 |   // Restriction schedule started 20 minutes ago and lasts for 2 hours. | 
 |   SetRestrictionSchedule(-base::Minutes(20), base::Hours(2)); | 
 |  | 
 |   ash::OobeScreenWaiter(ash::DeviceDisabledScreenView::kScreenId).Wait(); | 
 |  | 
 |   // Verify that it's really the restriction schedule banner showing, not the | 
 |   // standard device disabled one. | 
 |   OobeJS().ExpectElementText( | 
 |       l10n_util::GetStringUTF8( | 
 |           IDS_DEVICE_DISABLED_HEADING_RESTRICTION_SCHEDULE), | 
 |       kBannerTitle); | 
 | } | 
 |  | 
 | IN_PROC_BROWSER_TEST_F(DeviceRestrictionScheduleControllerTest, | 
 |                        RestrictedScheduleEnds) { | 
 |   // Restriction schedule started 20 minutes ago and lasts for 2 hours. | 
 |   SetRestrictionSchedule(-base::Minutes(20), base::Hours(2)); | 
 |  | 
 |   // DeviceDisabledScreen is shown. | 
 |   ash::OobeScreenWaiter(ash::DeviceDisabledScreenView::kScreenId).Wait(); | 
 |  | 
 |   // Reset the policy, restriction schedule ends. | 
 |   UpdatePolicy(kPolicyJsonEmpty); | 
 |  | 
 |   // In order to reset state and stop showing the DeviceDisabledScreen, Chrome | 
 |   // is restarted back to the login screen here. | 
 |   base::test::TestFuture<void> future; | 
 |   auto subscription = | 
 |       browser_shutdown::AddAppTerminatingCallback(future.GetCallback()); | 
 |   ASSERT_TRUE(future.Wait()); | 
 | } | 
 |  | 
 | IN_PROC_BROWSER_TEST_F(DeviceRestrictionScheduleControllerTest, | 
 |                        RestrictionScheduleMessageChanged) { | 
 |   auto mock_timer_owned = std::make_unique<FakeWallClockTimer>(); | 
 |   FakeWallClockTimer* mock_timer = mock_timer_owned.get(); | 
 |   controller().SetMessageUpdateTimerForTesting(std::move(mock_timer_owned)); | 
 |  | 
 |   base::SimpleTestClock test_clock; | 
 |   SetClocks(test_clock); | 
 |   test_clock.SetNow(base::Time::Now().LocalMidnight()); | 
 |  | 
 |   // Set the restriction schedule to start right now, and to last for 1.5 days. | 
 |   // Currently the message will contain "Tomorrow", and at next midnight will | 
 |   // contain "Today". | 
 |   SetRestrictionSchedule(base::TimeDelta(), base::Hours(36)); | 
 |  | 
 |   ash::OobeScreenWaiter(ash::DeviceDisabledScreenView::kScreenId).Wait(); | 
 |  | 
 |   OobeJS().ExpectElementContainsText( | 
 |       l10n_util::GetStringUTF8( | 
 |           IDS_DEVICE_DISABLED_EXPLANATION_RESTRICTION_SCHEDULE_TOMORROW), | 
 |       kBannerContents); | 
 |  | 
 |   // Update the time to tomorrow and fire the mock timer (+6 hours to avoid any | 
 |   // DST issues). | 
 |   test_clock.Advance(base::Days(1) + base::Hours(6)); | 
 |   mock_timer->FireNow(); | 
 |  | 
 |   OobeJS().ExpectElementContainsText( | 
 |       l10n_util::GetStringUTF8( | 
 |           IDS_DEVICE_DISABLED_EXPLANATION_RESTRICTION_SCHEDULE_TODAY), | 
 |       kBannerContents); | 
 |  | 
 |   // Reset the clocks back. | 
 |   SetClocks(*base::DefaultClock::GetInstance()); | 
 | } | 
 |  | 
 | }  // namespace policy |