| // 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 |