chromium/ios/chrome/browser/enterprise/model/idle/idle_service_unittest.mm

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

#import "ios/chrome/browser/enterprise/model/idle/idle_service.h"

#import "base/memory/raw_ptr.h"
#import "base/test/gmock_callback_support.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/time/time.h"
#import "components/enterprise/idle/idle_pref_names.h"
#import "components/enterprise/idle/metrics.h"
#import "ios/chrome/browser/enterprise/model/idle/action_runner.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/fake_authentication_service_delegate.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/browser/signin/model/fake_system_identity_manager.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/gmock/include/gmock/gmock.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"

using ::testing::_;

namespace enterprise_idle {

class ActionRunner;

// Tests that the idle service schedules tasks and runs actions as expected when
// the browser is starting up, re-foregrounded or already in foreground. Also
// tests that the actions run at the right time when the value of the
// IdleTimeout policy changes.
class IdleTimeoutServiceTest : public PlatformTest {
  // Mocks the `Run()` method which is used to check that actions run the
  // right time(s) in the tests.
  class MockActionRunner : public ActionRunner {
   public:
    MockActionRunner() {}
    MOCK_METHOD(void, Run, (ActionsCompletedCallback), (override));
    ~MockActionRunner() override {}
  };

 public:
  class MockObserver : public IdleService::Observer {
   public:
    MockObserver() {}
    ~MockObserver() override {}
    MOCK_METHOD(void, OnIdleTimeoutInForeground, (), (override));
    MOCK_METHOD(void, OnIdleTimeoutOnStartup, (), (override));
    MOCK_METHOD(void, OnIdleTimeoutActionsCompleted, (), (override));
    MOCK_METHOD(void, OnApplicationWillEnterBackground, (), (override));
  };

  IdleTimeoutServiceTest() = default;

  void SetIdleTimeoutPolicy(base::TimeDelta timeout) {
    base::Value::List actions;
    actions.Append(
        static_cast<int>(enterprise_idle::ActionType::kClearBrowsingHistory));
    browser_state_->GetPrefs()->SetList(
        enterprise_idle::prefs::kIdleTimeoutActions, std::move(actions));

    browser_state_.get()->GetPrefs()->SetTimeDelta(prefs::kIdleTimeout,
                                                   timeout);
  }

  void SetLastActiveTime(base::Time time) {
    local_state()->SetTime(prefs::kLastActiveTimestamp, time);
  }

  base::Time GetLastIdleTime() {
    return browser_state_.get()->GetPrefs()->GetTime(prefs::kLastIdleTimestamp);
  }

  void InitIdleService() {
    idle_service_ = std::make_unique<IdleService>(
        browser_state_->GetOriginalChromeBrowserState());
    idle_service_->SetActionRunnerForTesting(
        base::WrapUnique(new MockActionRunner()));
    action_runner_ = static_cast<MockActionRunner*>(
        idle_service_->GetActionRunnerForTesting());
    idle_service_->AddObserver(&mock_observer_);
  }

  void SignIn() {
    // Sign in.
    FakeSystemIdentity* identity = [FakeSystemIdentity fakeIdentity1];
    FakeSystemIdentityManager* system_identity_manager =
        FakeSystemIdentityManager::FromSystemIdentityManager(
            GetApplicationContext()->GetSystemIdentityManager());
    system_identity_manager->AddIdentity(identity);
    authentication_service_->SignIn(
        identity, signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN);
  }

  void SetUp() override {
    TestChromeBrowserState::Builder test_cbs_builder;
    test_cbs_builder.AddTestingFactory(
        AuthenticationServiceFactory::GetInstance(),
        AuthenticationServiceFactory::GetDefaultFactory());
    browser_state_ = std::move(test_cbs_builder).Build();
    AuthenticationServiceFactory::CreateAndInitializeForBrowserState(
        browser_state_.get(),
        std::make_unique<FakeAuthenticationServiceDelegate>());
    authentication_service_ = static_cast<AuthenticationService*>(
        AuthenticationServiceFactory::GetForBrowserState(browser_state_.get()));
  }

  void TearDown() override {
    idle_service_->RemoveObserver(&mock_observer_);
    idle_service_->Shutdown();
    idle_service_.reset();
    browser_state_.reset();
    base::RunLoop run_loop;
    run_loop.RunUntilIdle();
  }

  PrefService* local_state() {
    return GetApplicationContext()->GetLocalState();
  }

 protected:
  web::WebTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  MockObserver mock_observer_;
  raw_ptr<MockActionRunner> action_runner_;
  std::unique_ptr<TestChromeBrowserState> browser_state_;
  std::unique_ptr<IdleService> idle_service_;
  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
  raw_ptr<AuthenticationService> authentication_service_;
};

// Test that the observer methods are not called when the policy is not set, and
// that the idle check return false.
TEST_F(IdleTimeoutServiceTest, IdleTimeoutPolicyNotSet) {
  InitIdleService();
  // The pref value should not be positive if the policy is not set.
  ASSERT_FALSE(browser_state_->GetPrefs()
                   ->GetTimeDelta(enterprise_idle::prefs::kIdleTimeout)
                   .is_positive());
  EXPECT_CALL(*action_runner_, Run(_)).Times(0);
  EXPECT_CALL(mock_observer_, OnIdleTimeoutOnStartup()).Times(0);
  idle_service_->OnApplicationWillEnterForeground();
  EXPECT_CALL(mock_observer_, OnApplicationWillEnterBackground()).Times(0);
  idle_service_->OnApplicationWillEnterBackground();
  EXPECT_FALSE(idle_service_->IsIdleAfterPreviouslyBeingActive());
}

// When policy timeout is set after being unset.
TEST_F(IdleTimeoutServiceTest, IdleTimeoutPrefsSet_OnPolicySet) {
  InitIdleService();
  EXPECT_CALL(*action_runner_, Run(_)).Times(0);
  idle_service_->OnApplicationWillEnterForeground();
  task_environment_.FastForwardBy(base::Seconds(30));
  // Shorten timeout and expect immediate call to observers after pref change.
  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground()).Times(1);
  SetIdleTimeoutPolicy(base::Minutes(1));
  task_environment_.FastForwardBy(base::Seconds(30));
}

// When policy timeout is shortened, actions should run right away.
TEST_F(IdleTimeoutServiceTest, IdleTimeoutPrefsSet_OnPolicyChange) {
  SetIdleTimeoutPolicy(base::Minutes(3));
  InitIdleService();
  EXPECT_CALL(*action_runner_, Run(_)).Times(0);
  idle_service_->OnApplicationWillEnterForeground();

  // Shorten timeout and expect immediate call to observers after pref change.
  task_environment_.FastForwardBy(base::Minutes(2));
  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground());
  SetIdleTimeoutPolicy(base::Minutes(1));

  task_environment_.FastForwardBy(base::Seconds(30));
  // Increase the timeout again and make sure observers are not called except on
  // idle.
  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground()).Times(1);
  SetIdleTimeoutPolicy(base::Minutes(2));
  task_environment_.FastForwardBy(base::Minutes(1));
}

// Start-up Case: last active time and last idle time unset.
TEST_F(IdleTimeoutServiceTest, NoActionsRunOnStartup_FirstRunWithPolicySet) {
  SetIdleTimeoutPolicy(base::Minutes(1));
  InitIdleService();
  // No call expected when foregrounded.
  EXPECT_CALL(*action_runner_, Run(_)).Times(0);
  EXPECT_CALL(mock_observer_, OnIdleTimeoutOnStartup()).Times(0);
  idle_service_->OnApplicationWillEnterForeground();
  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground()).Times(2);
  task_environment_.FastForwardBy(base::Minutes(2));
}

// Start-up Case: current time < last active time + idle threshold.
TEST_F(IdleTimeoutServiceTest, NoActionsRunOnStartup_NoBackgroundTimeout) {
  // Sets the last active time to 30s ago.
  SetLastActiveTime(base::Time::Now() - base::Seconds(30));
  SetIdleTimeoutPolicy(base::Minutes(1));
  InitIdleService();

  EXPECT_CALL(mock_observer_, OnIdleTimeoutOnStartup()).Times(0);
  EXPECT_CALL(*action_runner_, Run(_)).Times(0);
  idle_service_->OnApplicationWillEnterForeground();
  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground()).Times(2);
  task_environment_.FastForwardBy(base::Minutes(2));
}

// Start-up Case: base::Time::Now() > last_active_time + idle threshold.
// Last idle time is not set.
TEST_F(IdleTimeoutServiceTest, ActionsRunOnStartup_PostBackgroundTimeout) {
  std::unique_ptr<base::HistogramTester> histogram_tester =
      std::make_unique<base::HistogramTester>();

  SetLastActiveTime(base::Time::Now() - base::Seconds(90));
  SetIdleTimeoutPolicy(base::Minutes(1));
  InitIdleService();

  EXPECT_CALL(mock_observer_, OnIdleTimeoutOnStartup());
  EXPECT_CALL(*action_runner_, Run(_)).Times(1);
  idle_service_->OnApplicationWillEnterForeground();
  // The background case should by recorded at this point.
  histogram_tester->ExpectUniqueSample(
      "Enterprise.IdleTimeoutPolicies.IdleTimeoutCase",
      metrics::IdleTimeoutCase::kBackground, 1);
  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground()).Times(2);
  task_environment_.FastForwardBy(base::Minutes(2));
  // The histogram should now have the previous timeout in background case and
  // the last two foreground cases.
  EXPECT_THAT(histogram_tester->GetAllSamples(
                  "Enterprise.IdleTimeoutPolicies.IdleTimeoutCase"),
              testing::ElementsAre(
                  base::Bucket(metrics::IdleTimeoutCase::kForeground, 2),
                  base::Bucket(metrics::IdleTimeoutCase::kBackground, 1)));
}

// Backgrund then reforeground case:
// base::Time::Now() - last_active_time < idle_threshold.
TEST_F(
    IdleTimeoutServiceTest,
    NoActionsRunInBackground_NoActionsRunOnReforegroundWithPrevUserActivity) {
  SetIdleTimeoutPolicy(base::Minutes(1));
  InitIdleService();
  idle_service_->OnApplicationWillEnterForeground();
  task_environment_.FastForwardBy(base::Seconds(30));
  // Simulate user activity.
  SetLastActiveTime(base::Time::Now());

  // Observers triggered on background (to dismiss any dialog).
  EXPECT_CALL(*action_runner_, Run(_)).Times(0);
  EXPECT_CALL(mock_observer_, OnApplicationWillEnterBackground()).Times(1);
  idle_service_->OnApplicationWillEnterBackground();
  task_environment_.FastForwardBy(base::Seconds(30));

  // Idle threshold was not exceeded as idle time = 30s.
  EXPECT_CALL(*action_runner_, Run(_)).Times(0);
  idle_service_->OnApplicationWillEnterForeground();

  // Ensure that idle state is detected after that.
  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground()).Times(1);
  EXPECT_CALL(*action_runner_, Run(_)).Times(1);
  task_environment_.FastForwardBy(base::Minutes(1));
  idle_service_->RunActions();
}

// Backgrund then reforeground case:
// last_idle_time > last_active_time + idle_threshold
TEST_F(IdleTimeoutServiceTest,
       NoActionsRunInBackground_NoActionsRunOnReforegrounWithPrevTimeout) {
  SetIdleTimeoutPolicy(base::Minutes(1));
  InitIdleService();
  idle_service_->OnApplicationWillEnterForeground();

  // Fast forward and invoke the `OnActionsCompleted` callback to set the last
  // idle time.
  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground()).Times(1);
  EXPECT_CALL(mock_observer_, OnIdleTimeoutActionsCompleted()).Times(1);
  task_environment_.FastForwardBy(base::Minutes(1));
  idle_service_->OnActionsCompleted();
  // `OnIdleTimeoutInForeground` should not be called again.
  task_environment_.FastForwardBy(base::Minutes(1));

  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground()).Times(0);
  idle_service_->OnApplicationWillEnterBackground();
  task_environment_.FastForwardBy(base::Minutes(1));

  // No action run on foreground because run was called last idle time and the
  // browser was not active after that. Reforegrounding sets the last active
  // time, so next time the idle state is triggered, actions will run.
  idle_service_->OnApplicationWillEnterForeground();
  // Ensure that idle state is detected after that but actions run after 1
  // minute of no activity since foregrounding.
  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground()).Times(1);
  task_environment_.FastForwardBy(base::Minutes(1));
}

// Backgrund then reforeground case:
// base::Time::Now() - last_active_time > idle_threshold
TEST_F(IdleTimeoutServiceTest,
       NoActionsRunInBackground_ActionsRunOnReforeground) {
  SetIdleTimeoutPolicy(base::Minutes(1));
  InitIdleService();
  idle_service_->OnApplicationWillEnterForeground();
  task_environment_.FastForwardBy(base::Seconds(30));
  // Simulate user activity.
  SetLastActiveTime(base::Time::Now());

  EXPECT_CALL(mock_observer_, OnApplicationWillEnterBackground()).Times(1);
  EXPECT_CALL(*action_runner_, Run(_)).Times(0);
  idle_service_->OnApplicationWillEnterBackground();
  task_environment_.FastForwardBy(base::Seconds(80));

  // Actions should run on startup (behind loading screen).
  EXPECT_CALL(*action_runner_, Run(_)).Times(1);
  EXPECT_CALL(mock_observer_, OnIdleTimeoutOnStartup()).Times(1);
  idle_service_->OnApplicationWillEnterForeground();

  // Ensure that idle state is detected after that.
  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground()).Times(1);
  task_environment_.FastForwardBy(base::Minutes(1));
}

// Foreground case: idle check should be scheduled for the right time when it
// might become idle.
TEST_F(IdleTimeoutServiceTest, ActionsRunAtCorrectTimesWhileForegrounded) {
  SetIdleTimeoutPolicy(base::Minutes(3));
  InitIdleService();
  idle_service_->OnApplicationWillEnterForeground();

  task_environment_.FastForwardBy(base::Seconds(40));
  // Simulate user activity at t=40s.
  SetLastActiveTime(base::Time::Now());

  // At t=3, actions will not run because idle time has not reached 3.
  // Should check again and run at t=3:40.
  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground()).Times(0);
  EXPECT_CALL(*action_runner_, Run(_)).Times(0);
  task_environment_.FastForwardBy(base::Minutes(3) - base::Seconds(40));

  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground()).Times(1);
  task_environment_.FastForwardBy(base::Seconds(40));
  idle_service_->OnActionsCompleted();

  // Since the actions have completed last time, and the user continued being
  // idle, observer should not be called.
  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground()).Times(0);
  task_environment_.FastForwardBy(base::Minutes(3));
}

// If there are observers for the service, the service should call
// `OnIdleTimeoutInForeground` without running the actions right away. The
// observer will decide if actions should run or not.
TEST_F(IdleTimeoutServiceTest,
       ActionsDoNotRunWhenObserverDoesNotInvokeCallback) {
  SetIdleTimeoutPolicy(base::Minutes(3));
  InitIdleService();
  idle_service_->OnApplicationWillEnterForeground();
  task_environment_.FastForwardBy(base::Seconds(40));
  // Simulate user activity at t=40s.
  SetLastActiveTime(base::Time::Now());

  // At t=3, actions will not run because idle time has not reached 3.
  // Should check again and run at t=3:40.
  EXPECT_CALL(*action_runner_, Run(_)).Times(0);
  task_environment_.FastForwardBy(base::Minutes(3) - base::Seconds(40));

  // Not running the callback passed to `OnIdleTimeoutInForeground` means no
  // actions should run.
  testing::InSequence in_sequence;
  EXPECT_CALL(mock_observer_, OnIdleTimeoutInForeground());
  EXPECT_CALL(*action_runner_, Run(_)).Times(0);
  task_environment_.FastForwardBy(base::Seconds(40));
}

// If the only action set is signout, and the user is not signed in when timeout
// is detected, the actions should not run.
TEST_F(IdleTimeoutServiceTest, NoActionsRunWhenNotNeeded_SignoutCase) {
  SetLastActiveTime(base::Time::Now() - base::Seconds(90));
  SetIdleTimeoutPolicy(base::Minutes(1));
  InitIdleService();
  base::Value::List actions;
  actions.Append(static_cast<int>(enterprise_idle::ActionType::kSignOut));
  browser_state_->GetPrefs()->SetList(
      enterprise_idle::prefs::kIdleTimeoutActions, std::move(actions));

  EXPECT_CALL(mock_observer_, OnIdleTimeoutOnStartup()).Times(0);
  EXPECT_CALL(*action_runner_, Run(_)).Times(0);
  idle_service_->OnApplicationWillEnterForeground();
}

// When policy timeout is set, but no action runs.
TEST_F(IdleTimeoutServiceTest, NoActionsRunWhenNotNeeded_UnknownActionsCase) {
  SetLastActiveTime(base::Time::Now() - base::Seconds(90));
  SetIdleTimeoutPolicy(base::Minutes(1));
  InitIdleService();
  base::Value::List actions;
  // This can be the case if a string value supported on desktop is the only
  // action that was set for the policy.
  browser_state_->GetPrefs()->SetList(
      enterprise_idle::prefs::kIdleTimeoutActions, std::move(actions));

  EXPECT_CALL(mock_observer_, OnIdleTimeoutOnStartup()).Times(0);
  EXPECT_CALL(*action_runner_, Run(_)).Times(0);
  idle_service_->OnApplicationWillEnterForeground();
}

// If the only action set is signout, and the user is signed in when timeout is
// detected, the actions should run.
TEST_F(IdleTimeoutServiceTest, ActionsRunWhenNeeded_OnlySignoutSet) {
  SetLastActiveTime(base::Time::Now() - base::Seconds(90));
  SetIdleTimeoutPolicy(base::Minutes(1));
  InitIdleService();
  SignIn();
  base::Value::List actions;
  actions.Append(static_cast<int>(enterprise_idle::ActionType::kSignOut));
  browser_state_->GetPrefs()->SetList(
      enterprise_idle::prefs::kIdleTimeoutActions, std::move(actions));

  EXPECT_CALL(mock_observer_, OnIdleTimeoutOnStartup());
  EXPECT_CALL(*action_runner_, Run(_)).Times(1);
  idle_service_->OnApplicationWillEnterForeground();
}

// If the only action set is not signout, the action should run regardless of
// the sign-in state.
TEST_F(IdleTimeoutServiceTest, ActionsRunWhenNeeded_OnlyActionSetIsNotSignOut) {
  SetLastActiveTime(base::Time::Now() - base::Seconds(90));
  SetIdleTimeoutPolicy(base::Minutes(1));
  InitIdleService();
  base::Value::List actions;
  actions.Append(
      static_cast<int>(enterprise_idle::ActionType::kClearBrowsingHistory));
  browser_state_->GetPrefs()->SetList(
      enterprise_idle::prefs::kIdleTimeoutActions, std::move(actions));

  EXPECT_CALL(mock_observer_, OnIdleTimeoutOnStartup());
  EXPECT_CALL(*action_runner_, Run(_)).Times(1);
  idle_service_->OnApplicationWillEnterForeground();
}

// If the only action set is signout, and the user is signed in when timeout is
// detected, the actions should run.
TEST_F(IdleTimeoutServiceTest,
       ActionsRunWhenNeeded_ManyActionsSetAndUserNotSignedIn) {
  SetLastActiveTime(base::Time::Now() - base::Seconds(90));
  SetIdleTimeoutPolicy(base::Minutes(1));
  InitIdleService();
  base::Value::List actions;
  actions.Append(
      static_cast<int>(enterprise_idle::ActionType::kClearBrowsingHistory));
  actions.Append(static_cast<int>(enterprise_idle::ActionType::kSignOut));
  browser_state_->GetPrefs()->SetList(
      enterprise_idle::prefs::kIdleTimeoutActions, std::move(actions));

  EXPECT_CALL(mock_observer_, OnIdleTimeoutOnStartup());
  EXPECT_CALL(*action_runner_, Run(_)).Times(1);
  idle_service_->OnApplicationWillEnterForeground();
}

}  // namespace enterprise_idle