chromium/ios/chrome/browser/ui/authentication/account_menu/account_menu_coordinator_unittests.mm

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

#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_coordinator.h"

#import <MaterialComponents/MaterialSnackbar.h>

#import "components/sync/service/sync_service_utils.h"
#import "components/trusted_vault/trusted_vault_server_constants.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/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/browser_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/quick_delete_commands.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.h"
#import "ios/chrome/browser/shared/public/commands/show_signin_command.h"
#import "ios/chrome/browser/shared/public/commands/snackbar_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/browser/signin/model/system_identity_manager.h"
#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_coordinator_delegate.h"
#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_mediator.h"
#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_mediator_delegate.h"
#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_view_controller.h"
#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_view_controller_presentation_delegate.h"
#import "ios/chrome/browser/ui/authentication/signout_action_sheet/signout_action_sheet_coordinator.h"
#import "ios/chrome/browser/ui/settings/sync/sync_encryption_passphrase_table_view_controller.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"

namespace {

const FakeSystemIdentity* kPrimaryIdentity = [FakeSystemIdentity fakeIdentity1];
const FakeSystemIdentity* kSecondaryIdentity =
    [FakeSystemIdentity fakeIdentity2];

}  // namespace

@interface AccountMenuCoordinator (Testing) <
    AccountMenuMediatorDelegate,
    AccountMenuViewControllerPresentationDelegate,
    SignoutActionSheetCoordinatorDelegate,
    UIAdaptivePresentationControllerDelegate,
    UINavigationControllerDelegate>

@property(nonatomic, weak) AccountMenuViewController* viewController;
@property(nonatomic, weak) AccountMenuMediator* mediator;

@end

class AccountMenuCoordinatorTest : public PlatformTest {
 public:
  void SetUp() override {
    PlatformTest::SetUp();

    TestChromeBrowserState::Builder builder;
    builder.AddTestingFactory(
        AuthenticationServiceFactory::GetInstance(),
        AuthenticationServiceFactory::GetDefaultFactory());
    browser_state_ = std::move(builder).Build();
    browser_ = std::make_unique<TestBrowser>(browser_state_.get());

    mock_application_commands_handler_ =
        OCMStrictProtocolMock(@protocol(ApplicationCommands));
    mock_snackbar_commands_handler_ =
        OCMStrictProtocolMock(@protocol(SnackbarCommands));
    mock_settings_commands_handler_ =
        OCMStrictProtocolMock(@protocol(SettingsCommands));
    mock_browser_commands_handler_ =
        OCMStrictProtocolMock(@protocol(BrowserCommands));
    CommandDispatcher* dispatcher = browser_->GetCommandDispatcher();
    [dispatcher startDispatchingToTarget:mock_application_commands_handler_
                             forProtocol:@protocol(ApplicationCommands)];
    [dispatcher startDispatchingToTarget:mock_snackbar_commands_handler_
                             forProtocol:@protocol(SnackbarCommands)];
    [dispatcher startDispatchingToTarget:mock_settings_commands_handler_
                             forProtocol:@protocol(SettingsCommands)];
    [dispatcher startDispatchingToTarget:mock_browser_commands_handler_
                             forProtocol:@protocol(BrowserCommands)];

    AuthenticationServiceFactory::CreateAndInitializeForBrowserState(
        browser_state_.get(),
        std::make_unique<FakeAuthenticationServiceDelegate>());
    fake_system_identity_manager_ =
        FakeSystemIdentityManager::FromSystemIdentityManager(
            GetApplicationContext()->GetSystemIdentityManager());
    authentication_service_ =
        AuthenticationServiceFactory::GetForBrowserState(browser_state_.get());

    SigninWithPrimaryIdentity();
    AddSecondaryIdentity();

    delegate_ =
        OCMStrictProtocolMock(@protocol(AccountMenuCoordinatorDelegate));
    coordinator_ = [[AccountMenuCoordinator alloc]
        initWithBaseViewController:nil
                           browser:browser_.get()];
    coordinator_.delegate = delegate_;

    [coordinator_ start];

    // Replacing the view controller and mediator by mock.
    view_ = coordinator_.viewController.view;
    view_controller_ = OCMStrictClassMock([AccountMenuViewController class]);
    OCMStub([view_controller_ view]).andReturn(view_);
    coordinator_.viewController = view_controller_;

    [coordinator_.mediator disconnect];
    mediator_ = OCMStrictClassMock([AccountMenuMediator class]);
    coordinator_.mediator = mediator_;
    OCMStub([mediator_ signOutFlowInProgress]).andReturn(NO);
    OCMStub([mediator_ addAccountOperationInProgress]).andReturn(NO);
  }

  void TearDown() override {
    OCMExpect(view_controller_.dataSource = nil);
    OCMExpect(view_controller_.delegate = nil);
    OCMExpect(view_controller_.mutator = nil);
    OCMExpect(mediator_.consumer = nil);
    OCMExpect([mediator_ disconnect]);
    OCMExpect(mediator_.delegate = nil);
    [coordinator_ stop];
    VerifyMock();
    PlatformTest::TearDown();
  }

 protected:
  void VerifyMock() {
    EXPECT_OCMOCK_VERIFY((id)delegate_);
    EXPECT_OCMOCK_VERIFY((id)mediator_);
    EXPECT_OCMOCK_VERIFY((id)view_controller_);
    EXPECT_OCMOCK_VERIFY((id)mock_application_commands_handler_);
    EXPECT_OCMOCK_VERIFY((id)mock_browser_commands_handler_);
    EXPECT_OCMOCK_VERIFY((id)mock_settings_commands_handler_);
    EXPECT_OCMOCK_VERIFY((id)mock_snackbar_commands_handler_);
  }

  AccountMenuCoordinator<UIAdaptivePresentationControllerDelegate>*
      coordinator_;
  id<AccountMenuCoordinatorDelegate> delegate_;
  id<ApplicationCommands> mock_application_commands_handler_;
  id<SnackbarCommands> mock_snackbar_commands_handler_;
  id<SettingsCommands> mock_settings_commands_handler_;
  id<BrowserCommands> mock_browser_commands_handler_;
  AccountMenuViewController* view_controller_;
  AccountMenuMediator* mediator_;
  AuthenticationService* authentication_service_;
  FakeSystemIdentityManager* fake_system_identity_manager_;
  // The view owned by the view controller.
  UIView* view_;

  // Expects `setSignoutFlowInProgress:in_progress` and ensure any request to
  // `signoutFlowInProgress` returns `in_progress`.
  void ExpectSetSignoutFlowInProgress(bool in_progress) {
    OCMExpect([mediator_ setSignOutFlowInProgress:in_progress])
        .andDo(^(NSInvocation* invocation) {
          OCMStub([mediator_ signOutFlowInProgress]).andReturn(in_progress);
        });
  }

  // Expects that the sign-out flow gets set to YES and then NO.
  void ExpectSetSignoutFlowInProgressOnAndOff() {
    ExpectSetSignoutFlowInProgress(YES);
    ExpectSetSignoutFlowInProgress(NO);
  }

  // Expects `setAddAccountOperationInProgress:in_progress` and ensure any
  // request to `addAccountOperationInProgress` returns `in_progress`.
  void ExpectAddAccountOperation(bool in_progress) {
    OCMExpect([mediator_ setAddAccountOperationInProgress:in_progress])
        .andDo(^(NSInvocation* invocation) {
          OCMStub([mediator_ addAccountOperationInProgress])
              .andReturn(in_progress);
        });
  }

 private:
  // Signs in kPrimaryIdentity as primary identity.
  void SigninWithPrimaryIdentity() {
    fake_system_identity_manager_->AddIdentity(kPrimaryIdentity);
    authentication_service_->SignIn(
        kPrimaryIdentity, signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN);
  }

  // Add kSecondaryIdentity as a secondary identity.
  void AddSecondaryIdentity() {
    fake_system_identity_manager_->AddIdentity(kSecondaryIdentity);
  }

  web::WebTaskEnvironment task_environment_;
  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
  std::unique_ptr<TestChromeBrowserState> browser_state_;
  std::unique_ptr<TestBrowser> browser_;
};

#pragma mark - AccountMenuViewControllerPresentationDelegate

// Tests view controller requesting to be closed.
TEST_F(AccountMenuCoordinatorTest, testWantsToBeClosed) {
  OCMExpect([delegate_ acountMenuCoordinatorShouldStop:coordinator_]);
  [coordinator_ viewControllerWantsToBeClosed:coordinator_.viewController];
}

// Tests that `didTapManageYourGoogleAccount` requests the view controller to
// present a view.
TEST_F(AccountMenuCoordinatorTest, testManageYourGoogleAccount) {
  OCMExpect([view_controller_ presentViewController:[OCMArg any]
                                           animated:YES
                                         completion:nil]);
  [coordinator_ didTapManageYourGoogleAccount];
}

// Tests that `didTapEditAccountList` has no impact on the view controller and
// mediator.
TEST_F(AccountMenuCoordinatorTest, testEditAccountList) {
  [coordinator_ didTapEditAccountList];
}

// Tests that `signOutFromTargetRect` requests the delegate to be stopped and
// shows a snackbar and calls its completion.
TEST_F(AccountMenuCoordinatorTest, testSignOut) {
  base::RunLoop run_loop;
  base::RepeatingClosure closure = run_loop.QuitClosure();
  CGRect rect = CGRect();
  OCMExpect([delegate_ acountMenuCoordinatorShouldStop:coordinator_]);
  OCMExpect([mock_snackbar_commands_handler_
      showSnackbarMessage:[OCMArg isNotNil]
             bottomOffset:0]);
  ExpectSetSignoutFlowInProgressOnAndOff();
  ExpectSetSignoutFlowInProgressOnAndOff();
  [coordinator_ signOutFromTargetRect:rect
                             callback:^(BOOL success) {
                               EXPECT_TRUE(success);
                               closure.Run();
                             }];
  run_loop.Run();
  EXPECT_EQ(authentication_service_->GetPrimaryIdentity(
                signin::ConsentLevel::kSignin),
            nil);
}

// Tests that `didTapAddAccount` requests the appllication commands handler to
// show signin.
TEST_F(AccountMenuCoordinatorTest, testAddAccount) {
  ExpectAddAccountOperation(YES);
  OCMExpect([mock_application_commands_handler_ showSignin:[OCMArg any]
                                        baseViewController:[OCMArg any]]);
  [coordinator_ didTapAddAccount];
}

#pragma mark - AccountMenuMediatorDelegate

// Tests that `mediatorWantsToBeDismissed` requests to the delegate to stop the
// coordinator.
TEST_F(AccountMenuCoordinatorTest, testMediatorWantsToBeDismissed) {
  OCMExpect([delegate_ acountMenuCoordinatorShouldStop:coordinator_]);
  [coordinator_ mediatorWantsToBeDismissed:coordinator_.mediator];
}

// Tests that `triggerSignoutWithTargetRect` shows a snackbar and calls its
// completion.
TEST_F(AccountMenuCoordinatorTest, testTriggerSignout) {
  OCMExpect([mock_snackbar_commands_handler_ showSnackbarMessage:[OCMArg any]
                                                    bottomOffset:0]);

  base::RunLoop run_loop;
  base::RepeatingClosure closure = run_loop.QuitClosure();
  CGRect rect = CGRect();
  ExpectSetSignoutFlowInProgressOnAndOff();
  ExpectSetSignoutFlowInProgressOnAndOff();
  [coordinator_ triggerSignoutWithTargetRect:rect
                                  completion:^(BOOL success) {
                                    EXPECT_TRUE(success);
                                    closure.Run();
                                  }];
  run_loop.Run();
}

// Tests that `triggerSigninWithSystemIdentity` call its completion.
TEST_F(AccountMenuCoordinatorTest, testSignin) {
  base::RunLoop run_loop;
  base::RepeatingClosure closure = run_loop.QuitClosure();
  [coordinator_
      triggerSigninWithSystemIdentity:kSecondaryIdentity
                           completion:^(id<SystemIdentity> systemIdentity) {
                             EXPECT_EQ(systemIdentity, kSecondaryIdentity);
                             closure.Run();
                           }];

  run_loop.Run();
}

// Tests that `triggerAccountSwitchSnackbarWithIdentity` shows a snackbar.
TEST_F(AccountMenuCoordinatorTest, testSnackbar) {
  OCMExpect([mock_snackbar_commands_handler_
      showSnackbarMessageOverBrowserToolbar:
          [OCMArg isKindOfClass:[MDCSnackbarMessage class]]]);
  [coordinator_ triggerAccountSwitchSnackbarWithIdentity:kPrimaryIdentity];
}

#pragma mark - SyncErrorSettingsCommandHandler

// Tests that `openPassphraseDialogWithModalPresentation` has no impact on the
// view controller and mediator. Tests also that the
// `SyncEncryptionPassphraseTableViewController` is allocated, and the view is
// correctly closed when the coordinator is stopped.
TEST_F(AccountMenuCoordinatorTest, testPassphrase) {
  SyncEncryptionPassphraseTableViewController* passphraseViewController =
      [SyncEncryptionPassphraseTableViewController alloc];
  id classMock =
      OCMClassMock([SyncEncryptionPassphraseTableViewController class]);
  OCMStub([classMock alloc]).andReturn(passphraseViewController);
  [coordinator_ openPassphraseDialogWithModalPresentation:YES];
}

// Tests that `openTrustedVaultReauthForFetchKeys` calls
// `showTrustedVaultReauthForFetchKeysFromViewController`.
TEST_F(AccountMenuCoordinatorTest, testFetchKeys) {
  OCMExpect([mock_application_commands_handler_
      showTrustedVaultReauthForFetchKeysFromViewController:[OCMArg any]
                                          securityDomainID:
                                              trusted_vault::SecurityDomainId::
                                                  kChromeSync
                                                   trigger:
                                                       syncer::
                                                           TrustedVaultUserActionTriggerForUMA::
                                                               kSettings
                                               accessPoint:
                                                   signin_metrics::AccessPoint::
                                                       ACCESS_POINT_ACCOUNT_MENU]);
  [coordinator_ openTrustedVaultReauthForFetchKeys];
}

// Tests that `openTrustedVaultReauthForDegradedRecoverability` calls
// `showTrustedVaultReauthForDegradedRecoverabilityFromViewController`.
TEST_F(AccountMenuCoordinatorTest, testDegradedRecoverability) {
  OCMExpect([mock_application_commands_handler_
      showTrustedVaultReauthForDegradedRecoverabilityFromViewController:[OCMArg
                                                                            any]

                                                       securityDomainID:
                                                           trusted_vault::
                                                               SecurityDomainId::
                                                                   kChromeSync
                                                                trigger:
                                                                    syncer::
                                                                        TrustedVaultUserActionTriggerForUMA::
                                                                            kSettings
                                                            accessPoint:
                                                                signin_metrics::
                                                                    AccessPoint::
                                                                        ACCESS_POINT_ACCOUNT_MENU]);
  [coordinator_ openTrustedVaultReauthForDegradedRecoverability];
}

// Tests that `openMDMErrodDialogWithSystemIdentity` has no effects on the
// mediator and view controller.
TEST_F(AccountMenuCoordinatorTest, testMDMError) {
  [coordinator_ openMDMErrodDialogWithSystemIdentity:kPrimaryIdentity];
}

// Tests that `openPrimaryAccountReauthDialog` calls `showSignin`.
TEST_F(AccountMenuCoordinatorTest, testReauth) {
  OCMExpect([mock_application_commands_handler_
              showSignin:[OCMArg checkWithBlock:^BOOL(
                                     ShowSigninCommand* value) {
                return value.operation ==
                           AuthenticationOperation::kPrimaryAccountReauth &&
                       value.accessPoint == signin_metrics::AccessPoint::
                                                ACCESS_POINT_ACCOUNT_MENU &&
                       value.identity == nil;
              }]
      baseViewController:[OCMArg any]]);
  [coordinator_ openPrimaryAccountReauthDialog];
}