chromium/ios/chrome/browser/policy/model/policy_watcher_browser_agent_unittest.mm

// Copyright 2021 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/policy/model/policy_watcher_browser_agent.h"

#import "base/apple/backup_util.h"
#import "base/apple/foundation_util.h"
#import "base/memory/raw_ptr.h"
#import "base/path_service.h"
#import "base/test/ios/wait_util.h"
#import "build/build_config.h"
#import "components/pref_registry/pref_registry_syncable.h"
#import "components/prefs/pref_service.h"
#import "components/sync/base/pref_names.h"
#import "components/sync_preferences/pref_service_mock_factory.h"
#import "components/sync_preferences/pref_service_syncable.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/browser/policy/model/policy_util.h"
#import "ios/chrome/browser/policy/model/policy_watcher_browser_agent_observer_bridge.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/prefs/browser_prefs.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/policy_change_commands.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/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"

using sync_preferences::PrefServiceMockFactory;
using sync_preferences::PrefServiceSyncable;
using user_prefs::PrefRegistrySyncable;
using web::WebTaskEnvironment;

class PolicyWatcherBrowserAgentTest : public PlatformTest {
 protected:
  void SetUp() override {
    PlatformTest::SetUp();
    TestChromeBrowserState::Builder builder;
    builder.SetPrefService(CreatePrefService());
    builder.AddTestingFactory(
        AuthenticationServiceFactory::GetInstance(),
        AuthenticationServiceFactory::GetDefaultFactory());
    chrome_browser_state_ = std::move(builder).Build();

    AuthenticationServiceFactory::CreateAndInitializeForBrowserState(
        chrome_browser_state_.get(),
        std::make_unique<FakeAuthenticationServiceDelegate>());
    // Set the initial pref value.
    GetLocalState()->SetInteger(prefs::kBrowserSigninPolicy,
                                static_cast<int>(BrowserSigninMode::kEnabled));

    // SceneState.
    app_state_ = [[AppState alloc] initWithStartupInformation:nil];
    scene_state_ = [[SceneState alloc] initWithAppState:app_state_];
    scene_state_.activationLevel = SceneActivationLevelForegroundActive;

    // Set up the test browser and attach the browser agents.
    browser_ = std::make_unique<TestBrowser>(chrome_browser_state_.get(),
                                             scene_state_);

    // Browser Agent under test.
    PolicyWatcherBrowserAgent::CreateForBrowser(browser_.get());
    agent_ = PolicyWatcherBrowserAgent::FromBrowser(browser_.get());
  }

  std::unique_ptr<PrefServiceSyncable> CreatePrefService() {
    PrefServiceMockFactory factory;
    scoped_refptr<PrefRegistrySyncable> registry(new PrefRegistrySyncable);
    std::unique_ptr<PrefServiceSyncable> prefs =
        factory.CreateSyncable(registry.get());
    RegisterBrowserStatePrefs(registry.get());
    return prefs;
  }

  // Sign in in the authentication service with a fake identity.
  void SignIn() {
    FakeSystemIdentity* identity = [FakeSystemIdentity fakeIdentity1];
    FakeSystemIdentityManager* system_identity_manager =
        FakeSystemIdentityManager::FromSystemIdentityManager(
            GetApplicationContext()->GetSystemIdentityManager());
    system_identity_manager->AddIdentity(identity);
    AuthenticationServiceFactory::GetForBrowserState(
        chrome_browser_state_.get())
        ->SignIn(identity, signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN);
  }

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

  web::WebTaskEnvironment task_environment_;
  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
  std::unique_ptr<TestChromeBrowserState> chrome_browser_state_;
  raw_ptr<PolicyWatcherBrowserAgent> agent_;
  std::unique_ptr<Browser> browser_;
  SceneState* scene_state_;
  // Keep app_state_ alive as it is a weak property of the scene state.
  AppState* app_state_;
};

#pragma mark - Tests.

// Tests that the browser agent doesn't monitor the pref if Initialize hasn't
// been called.
TEST_F(PolicyWatcherBrowserAgentTest, NoObservationIfNoInitialize) {
  // Set the initial pref value.
  GetLocalState()->SetInteger(prefs::kBrowserSigninPolicy,
                              static_cast<int>(BrowserSigninMode::kEnabled));

  // Set up the test browser and attach the browser agent under test.
  std::unique_ptr<Browser> browser =
      std::make_unique<TestBrowser>(chrome_browser_state_.get());
  PolicyWatcherBrowserAgent::CreateForBrowser(browser.get());

  // Set up the mock observer handler as strict mock. Calling it will fail the
  // test.
  id mockObserver =
      OCMStrictProtocolMock(@protocol(PolicyWatcherBrowserAgentObserving));
  PolicyWatcherBrowserAgentObserverBridge bridge(mockObserver);
  agent_->AddObserver(&bridge);

  // Action: disable browser sign-in.
  GetLocalState()->SetInteger(prefs::kBrowserSigninPolicy,
                              static_cast<int>(BrowserSigninMode::kDisabled));

  agent_->RemoveObserver(&bridge);
}

// Tests that the browser agent monitors the kBrowserSigninPolicy pref and
// notifies its observers when it changes.
TEST_F(PolicyWatcherBrowserAgentTest, ObservesSigninAllowedByPolicy) {
  // Set the initial pref value.
  GetLocalState()->SetInteger(prefs::kBrowserSigninPolicy,
                              static_cast<int>(BrowserSigninMode::kEnabled));
  // Set up the mock observer handler.
  id mockObserver =
      OCMStrictProtocolMock(@protocol(PolicyWatcherBrowserAgentObserving));
  PolicyWatcherBrowserAgentObserverBridge bridge(mockObserver);
  agent_->AddObserver(&bridge);
  id mockHandler = OCMProtocolMock(@protocol(PolicyChangeCommands));
  agent_->Initialize(mockHandler);

  // Setup the expectation after the Initialize to make sure that the observers
  // are notified when the pref is updated and not during Initialize().
  OCMExpect(
      [mockObserver policyWatcherBrowserAgentNotifySignInDisabled:agent_]);

  // Action: disable browser sign-in.
  GetLocalState()->SetInteger(prefs::kBrowserSigninPolicy,
                              static_cast<int>(BrowserSigninMode::kDisabled));

  // Verify the forceSignOut command was dispatched by the browser agent.
  EXPECT_OCMOCK_VERIFY(mockObserver);

  agent_->RemoveObserver(&bridge);
}

// Tests that the pref change doesn't trigger a command if the user isn't signed
// in.
TEST_F(PolicyWatcherBrowserAgentTest, NoCommandIfNotSignedIn) {
  AuthenticationService* authentication_service =
      AuthenticationServiceFactory::GetForBrowserState(
          chrome_browser_state_.get());

  ASSERT_FALSE(authentication_service->HasPrimaryIdentity(
      signin::ConsentLevel::kSignin));

  // Strict mock, will fail if a method is called.
  id mockHandler = OCMStrictProtocolMock(@protocol(PolicyChangeCommands));
  agent_->Initialize(mockHandler);

  // Action: disable browser sign-in.
  GetLocalState()->SetInteger(prefs::kBrowserSigninPolicy,
                              static_cast<int>(BrowserSigninMode::kDisabled));
}

// Tests that the pref change triggers a command if the user is signed
// in.
TEST_F(PolicyWatcherBrowserAgentTest, CommandIfSignedIn) {
  AuthenticationService* authentication_service =
      AuthenticationServiceFactory::GetForBrowserState(
          chrome_browser_state_.get());

  SignIn();

  ASSERT_TRUE(authentication_service->HasPrimaryIdentity(
      signin::ConsentLevel::kSignin));

  id mockHandler = OCMProtocolMock(@protocol(PolicyChangeCommands));
  agent_->Initialize(mockHandler);

  base::RunLoop run_loop;
  base::RunLoop* run_loop_ptr = &run_loop;
  OCMExpect([mockHandler showForceSignedOutPrompt]).andDo(^(NSInvocation*) {
    run_loop_ptr->Quit();
  });

  // Action: disable browser sign-in.
  GetLocalState()->SetInteger(prefs::kBrowserSigninPolicy,
                              static_cast<int>(BrowserSigninMode::kDisabled));
  run_loop.Run();

  // Verify the forceSignOut command was dispatched by the browser agent.
  EXPECT_OCMOCK_VERIFY(mockHandler);
  EXPECT_FALSE(authentication_service->HasPrimaryIdentity(
      signin::ConsentLevel::kSignin));
}

// Tests that the pref change doesn't trigger a command if the scene isn't
// active.
TEST_F(PolicyWatcherBrowserAgentTest, NoCommandIfNotActive) {
  AuthenticationService* authentication_service =
      AuthenticationServiceFactory::GetForBrowserState(
          chrome_browser_state_.get());

  scene_state_.activationLevel = SceneActivationLevelForegroundInactive;

  SignIn();

  ASSERT_TRUE(authentication_service->HasPrimaryIdentity(
      signin::ConsentLevel::kSignin));

  // Strict mock, will fail if a method is called.
  id mockHandler = OCMStrictProtocolMock(@protocol(PolicyChangeCommands));
  agent_->Initialize(mockHandler);

  // Action: disable browser sign-in.
  GetLocalState()->SetInteger(prefs::kBrowserSigninPolicy,
                              static_cast<int>(BrowserSigninMode::kDisabled));

  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      base::test::ios::kWaitForActionTimeout, ^bool {
        base::RunLoop().RunUntilIdle();
        return scene_state_.appState.shouldShowForceSignOutPrompt;
      }));
  EXPECT_FALSE(authentication_service->HasPrimaryIdentity(
      signin::ConsentLevel::kSignin));
}

// Tests that the handler is called and the user signed out if the policy is
// updated while the app is not running.
TEST_F(PolicyWatcherBrowserAgentTest, SignOutIfPolicyChangedAtColdStart) {
  // Create another Agent from a new browser to simulate a behaviour of "the
  // pref changed in background.

  AuthenticationService* authentication_service =
      AuthenticationServiceFactory::GetForBrowserState(
          chrome_browser_state_.get());
  SignIn();

  // Update the pref and Sign in.
  GetLocalState()->SetInteger(prefs::kBrowserSigninPolicy,
                              static_cast<int>(BrowserSigninMode::kDisabled));

  // Set up the test browser and attach the browser agents.
  SceneState* scene_state = [[SceneState alloc] initWithAppState:app_state_];
  scene_state.activationLevel = SceneActivationLevelForegroundActive;
  std::unique_ptr<Browser> browser =
      std::make_unique<TestBrowser>(chrome_browser_state_.get(), scene_state);

  // Browser Agent under test.
  PolicyWatcherBrowserAgent::CreateForBrowser(browser.get());
  PolicyWatcherBrowserAgent* agent =
      PolicyWatcherBrowserAgent::FromBrowser(browser.get());

  // The SignOut will occur when the handler is set.
  ASSERT_TRUE(authentication_service->HasPrimaryIdentity(
      signin::ConsentLevel::kSignin));

  id mockHandler = OCMProtocolMock(@protocol(PolicyChangeCommands));
  base::RunLoop run_loop;
  base::RunLoop* run_loop_ptr = &run_loop;
  OCMExpect([mockHandler showForceSignedOutPrompt]).andDo(^(NSInvocation*) {
    run_loop_ptr->Quit();
  });
  agent->Initialize(mockHandler);
  run_loop.Run();

  EXPECT_OCMOCK_VERIFY(mockHandler);
  EXPECT_FALSE(authentication_service->HasPrimaryIdentity(
      signin::ConsentLevel::kSignin));
}

// Tests that the command to show the UI isn't sent if the authentication
// service is still signing out the user.
TEST_F(PolicyWatcherBrowserAgentTest, UINotShownWhileSignOut) {
  AuthenticationService* authentication_service =
      static_cast<AuthenticationService*>(
          AuthenticationServiceFactory::GetForBrowserState(
              chrome_browser_state_.get()));

  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);
  ASSERT_TRUE(authentication_service->HasPrimaryIdentity(
      signin::ConsentLevel::kSignin));

  GetLocalState()->SetInteger(prefs::kBrowserSigninPolicy,
                              static_cast<int>(BrowserSigninMode::kDisabled));

  // Strict protocol: method calls will fail until the method is stubbed.
  id mockHandler = OCMStrictProtocolMock(@protocol(PolicyChangeCommands));
  agent_->Initialize(mockHandler);

  // As the SignOut callback hasn't been called yet, this shouldn't trigger a UI
  // update.
  agent_->SignInUIDismissed();

  OCMExpect([mockHandler showForceSignedOutPrompt]);

  base::RunLoop().RunUntilIdle();
  ASSERT_FALSE(authentication_service->HasPrimaryIdentity(
      signin::ConsentLevel::kSignin));

  // Once the SignOut callback is executed, the command should be sent.
  EXPECT_OCMOCK_VERIFY(mockHandler);
}

// Tests that the command to show the UI is sent when the Browser Agent is
// notified of the UI being dismissed.
TEST_F(PolicyWatcherBrowserAgentTest, CommandSentWhenUIIsDismissed) {
  SignIn();

  GetLocalState()->SetInteger(prefs::kBrowserSigninPolicy,
                              static_cast<int>(BrowserSigninMode::kDisabled));

  // Strict protocol: method calls will fail until the method is stubbed.
  id mockHandler = OCMStrictProtocolMock(@protocol(PolicyChangeCommands));
  base::RunLoop run_loop;
  base::RunLoop* run_loop_ptr = &run_loop;
  OCMExpect([mockHandler showForceSignedOutPrompt]).andDo(^(NSInvocation*) {
    run_loop_ptr->Quit();
  });
  agent_->Initialize(mockHandler);
  run_loop.Run();

  EXPECT_OCMOCK_VERIFY(mockHandler);

  // Reset the expectation for the SignInUIDismissed call.
  OCMExpect([mockHandler showForceSignedOutPrompt]);

  agent_->SignInUIDismissed();

  EXPECT_OCMOCK_VERIFY(mockHandler);
}

// Tests that the handler is called and the alert shown as expected.
TEST_F(PolicyWatcherBrowserAgentTest, AlertIfSyncDisabledChanges) {
  // Make sure shown if off.
  NSUserDefaults* standard_defaults = [NSUserDefaults standardUserDefaults];
  [standard_defaults setBool:NO forKey:kSyncDisabledAlertShownKey];
  browser_->GetBrowserState()->GetPrefs()->SetBoolean(
      syncer::prefs::internal::kSyncManaged, false);

  // Browser Agent under test.
  // Set up the test browser and attach the browser agents.
  SceneState* scene_state = [[SceneState alloc] initWithAppState:app_state_];
  scene_state.activationLevel = SceneActivationLevelForegroundActive;
  std::unique_ptr<Browser> browser =
      std::make_unique<TestBrowser>(chrome_browser_state_.get(), scene_state);

  // Browser Agent under test.
  PolicyWatcherBrowserAgent::CreateForBrowser(browser.get());
  PolicyWatcherBrowserAgent* agent =
      PolicyWatcherBrowserAgent::FromBrowser(browser.get());

  @autoreleasepool {
    id mockHandler = OCMProtocolMock(@protocol(PolicyChangeCommands));
    OCMExpect([mockHandler showSyncDisabledPrompt]);
    agent->Initialize(mockHandler);

    // Update the pref.
    browser_->GetBrowserState()->GetPrefs()->SetBoolean(
        syncer::prefs::internal::kSyncManaged, true);

    EXPECT_OCMOCK_VERIFY(mockHandler);
    EXPECT_TRUE([standard_defaults boolForKey:kSyncDisabledAlertShownKey]);

    [[mockHandler reject] showSyncDisabledPrompt];

    // Update the pref.
    browser_->GetBrowserState()->GetPrefs()->SetBoolean(
        syncer::prefs::internal::kSyncManaged, false);

    EXPECT_OCMOCK_VERIFY(mockHandler);
    EXPECT_FALSE([standard_defaults boolForKey:kSyncDisabledAlertShownKey]);
  }
}

// Tests that the handler is called and the alert shown at startup as expected.
TEST_F(PolicyWatcherBrowserAgentTest, AlertIfSyncDisabledChangedAtColdStart) {
  // Make sure shown if off.
  NSUserDefaults* standard_defaults = [NSUserDefaults standardUserDefaults];
  [standard_defaults setBool:NO forKey:kSyncDisabledAlertShownKey];
  browser_->GetBrowserState()->GetPrefs()->SetBoolean(
      syncer::prefs::internal::kSyncManaged, true);

  // Browser Agent under test.
  // Set up the test browser and attach the browser agents.
  SceneState* scene_state = [[SceneState alloc] initWithAppState:app_state_];
  scene_state.activationLevel = SceneActivationLevelForegroundActive;
  std::unique_ptr<Browser> browser =
      std::make_unique<TestBrowser>(chrome_browser_state_.get(), scene_state);

  // Browser Agent under test.
  PolicyWatcherBrowserAgent::CreateForBrowser(browser.get());
  PolicyWatcherBrowserAgent* agent =
      PolicyWatcherBrowserAgent::FromBrowser(browser.get());

  @autoreleasepool {
    id mockHandler = OCMProtocolMock(@protocol(PolicyChangeCommands));
    OCMExpect([mockHandler showSyncDisabledPrompt]);
    agent->Initialize(mockHandler);

    base::RunLoop().RunUntilIdle();

    EXPECT_OCMOCK_VERIFY(mockHandler);
    EXPECT_TRUE([standard_defaults boolForKey:kSyncDisabledAlertShownKey]);

    [[mockHandler reject] showSyncDisabledPrompt];

    // Update the pref.
    browser_->GetBrowserState()->GetPrefs()->SetBoolean(
        syncer::prefs::internal::kSyncManaged, false);

    EXPECT_OCMOCK_VERIFY(mockHandler);
    EXPECT_FALSE([standard_defaults boolForKey:kSyncDisabledAlertShownKey]);
  }
}

// Tests that disabling the backup-allowed preference marks the app container
// as excluded from backup, and enabling the preference clears this exclusion.
TEST_F(PolicyWatcherBrowserAgentTest, BackupPreventionChanged) {
  id mockHandler = OCMProtocolMock(@protocol(PolicyChangeCommands));
  agent_->Initialize(mockHandler);
  base::FilePath storage_dir = base::apple::GetUserLibraryPath();

  // Ensure that backups are allowed initially.
  ASSERT_TRUE(base::apple::ClearBackupExclusion(storage_dir));

  // Disallow backups.
  browser_->GetBrowserState()->GetPrefs()->SetBoolean(
      prefs::kAllowChromeDataInBackups, false);
  task_environment_.RunUntilIdle();
  EXPECT_TRUE(base::apple::GetBackupExclusion(storage_dir));

  // Allow backups.
  browser_->GetBrowserState()->GetPrefs()->SetBoolean(
      prefs::kAllowChromeDataInBackups, true);
  task_environment_.RunUntilIdle();
  EXPECT_FALSE(base::apple::GetBackupExclusion(storage_dir));
}

// Tests that disabling the backup-allowed preference marks the app container
// as excluded from backup at startup.
TEST_F(PolicyWatcherBrowserAgentTest, BackupDisallowedAtColdStart) {
  base::FilePath storage_dir = base::apple::GetUserLibraryPath();

  // Ensure that backups are allowed initially.
  ASSERT_TRUE(base::apple::ClearBackupExclusion(storage_dir));

  // Disallow backups
  browser_->GetBrowserState()->GetPrefs()->SetBoolean(
      prefs::kAllowChromeDataInBackups, false);

  id mockHandler = OCMProtocolMock(@protocol(PolicyChangeCommands));
  agent_->Initialize(mockHandler);
  task_environment_.RunUntilIdle();
  EXPECT_TRUE(base::apple::GetBackupExclusion(storage_dir));
}

// Tests that enabling the backup-allowed preference marks the app container
// as no longer excluded from backup at startup.
TEST_F(PolicyWatcherBrowserAgentTest, BackupAllowedAtColdStart) {
  base::FilePath storage_dir = base::apple::GetUserLibraryPath();

  // Ensure that backups are disallowed initially.
  ASSERT_TRUE(base::apple::SetBackupExclusion(storage_dir));

  // Allow backups
  browser_->GetBrowserState()->GetPrefs()->SetBoolean(
      prefs::kAllowChromeDataInBackups, true);

  id mockHandler = OCMProtocolMock(@protocol(PolicyChangeCommands));
  agent_->Initialize(mockHandler);
  task_environment_.RunUntilIdle();
  EXPECT_FALSE(base::apple::GetBackupExclusion(storage_dir));
}