chromium/ios/chrome/browser/ui/authentication/account_menu/account_menu_view_controller_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_view_controller.h"

#import "base/check_op.h"
#import "base/test/metrics/user_action_tester.h"
#import "ios/chrome/browser/policy/model/management_state.h"
#import "ios/chrome/browser/settings/model/sync/utils/account_error_ui_info.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/shared/ui/table_view/cells/table_view_text_item.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.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/chrome_account_manager_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_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/ui/authentication/account_menu/account_menu_data_source.h"
#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_mutator.h"
#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_view_controller_presentation_delegate.h"
#import "ios/chrome/browser/ui/authentication/cells/central_account_view.h"
#import "ios/chrome/browser/ui/authentication/cells/table_view_account_item.h"
#import "ios/chrome/browser/ui/settings/cells/settings_image_detail_text_cell.h"
#import "ios/chrome/browser/ui/settings/settings_table_view_controller_constants.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

const FakeSystemIdentity* kPrimaryIdentity = [FakeSystemIdentity fakeIdentity1];
const FakeSystemIdentity* kSecondaryIdentity =
    [FakeSystemIdentity fakeIdentity2];
const FakeSystemIdentity* kSecondaryIdentity2 =
    [FakeSystemIdentity fakeIdentity3];
UIImage* kPrimaryAccountAvatar = [[UIImage alloc] init];

}  // namespace

// An account menu data source with a primary and a secondary identities.
@interface FakeAccountMenuDataSource : NSObject <AccountMenuDataSource>
@property(nonatomic, assign) ChromeAccountManagerService* accountManagerService;
@property(nonatomic, strong) AccountErrorUIInfo* accountErrorUIInfo;
@end

@implementation FakeAccountMenuDataSource
@synthesize secondaryAccountsGaiaIDs = _secondaryAccountsGaiaIDs;
@synthesize primaryAccountEmail = _primaryAccountEmail;
@synthesize primaryAccountAvatar = _primaryAccountAvatar;
@synthesize primaryAccountUserFullName = _primaryAccountUserFullName;
@synthesize managementState = _managementState;

- (instancetype)init {
  self = [super init];
  if (self) {
    _accountErrorUIInfo = nil;
    _secondaryAccountsGaiaIDs = @[ kSecondaryIdentity.gaiaID ];
    _primaryAccountEmail = kPrimaryIdentity.userEmail;
    _primaryAccountAvatar = kPrimaryAccountAvatar;
    _primaryAccountUserFullName = kPrimaryIdentity.userFullName;
    _managementState.user_level_domain = "acme.com";
  }
  return self;
}

// The only acceptable argument is the ID of a secondary id.
- (TableViewAccountItem*)identityItemForGaiaID:(NSString*)gaiaID {
  const FakeSystemIdentity* identity;
  if (gaiaID == kSecondaryIdentity.gaiaID) {
    identity = kSecondaryIdentity;
  } else if (gaiaID == kSecondaryIdentity2.gaiaID) {
    identity = kSecondaryIdentity2;
  } else {
    NOTREACHED();
  }
  TableViewAccountItem* item =
      [[TableViewAccountItem alloc] initWithType:SettingsItemTypeAccount];
  item.text = identity.userFullName;
  item.detailText = identity.userEmail;
  item.image = _accountManagerService->GetIdentityAvatarWithIdentity(
      identity, IdentityAvatarSize::Regular);
  return item;
}
@end

class AccountMenuViewControllerTest : public PlatformTest {
 public:
  void SetUp() override {
    PlatformTest::SetUp();
    TestChromeBrowserState::Builder builder;
    builder.AddTestingFactory(
        AuthenticationServiceFactory::GetInstance(),
        AuthenticationServiceFactory::GetDefaultFactory());
    browser_state_ = std::move(builder).Build();
    fake_system_identity_manager_ =
        FakeSystemIdentityManager::FromSystemIdentityManager(
            GetApplicationContext()->GetSystemIdentityManager());
    AuthenticationServiceFactory::CreateAndInitializeForBrowserState(
        browser_state_.get(),
        std::make_unique<FakeAuthenticationServiceDelegate>());
    data_source_.accountManagerService =
        ChromeAccountManagerServiceFactory::GetForBrowserState(
            browser_state_.get());
    authentication_service_ =
        AuthenticationServiceFactory::GetForBrowserState(browser_state_.get());

    AddPrimaryIdentity();
    AddSecondaryIdentity();

    view_controller_ = [[AccountMenuViewController alloc]
        initWithStyle:ChromeTableViewStyle()];
    delegate_ = OCMStrictProtocolMock(
        @protocol(AccountMenuViewControllerPresentationDelegate));
    mutator_ = OCMStrictProtocolMock(@protocol(AccountMenuMutator));

    view_controller_.dataSource = data_source_;
    view_controller_.mutator = mutator_;
    view_controller_.delegate = delegate_;
    [view_controller_ viewDidLoad];
  }

  void TearDown() override {
    VerifyMock();
    PlatformTest::TearDown();
  }

 protected:
  AccountMenuViewController* view_controller_;
  id<AccountMenuViewControllerPresentationDelegate> delegate_;
  ChromeAccountManagerService* account_manager_service_;
  id<AccountMenuMutator> mutator_;
  FakeAccountMenuDataSource* data_source_ =
      [[FakeAccountMenuDataSource alloc] init];
  NSIndexPath* path_for_secondary_account_ = [NSIndexPath indexPathForRow:0
                                                                inSection:0];
  NSIndexPath* path_for_sign_out_ = [NSIndexPath indexPathForRow:0 inSection:1];
  NSIndexPath* path_for_add_account_ = [NSIndexPath indexPathForRow:1
                                                          inSection:0];
  AuthenticationService* authentication_service_;
  FakeSystemIdentityManager* fake_system_identity_manager_;
  base::UserActionTester user_actions_;

  // Verify that all mocks expectation are fulfilled.
  void VerifyMock() {
    EXPECT_OCMOCK_VERIFY((id)delegate_);
    EXPECT_OCMOCK_VERIFY((id)mutator_);
  }

  // The UITableView* of the account menu view controller.
  UITableView* TableView() { return view_controller_.tableView; }

  //  Returns the cell at `path`.
  UITableViewCell* GetCell(NSIndexPath* path) {
    return [TableView().dataSource tableView:TableView()
                       cellForRowAtIndexPath:path];
  }

  // Expects that the cell at `path` is a `TableViewTextCell` whose label’s text
  // is `text`.
  void ExpectTextAtPath(NSString* text, NSIndexPath* path) {
    UITableViewCell* add_account_cell_ = GetCell(path);
    EXPECT_TRUE([add_account_cell_ isKindOfClass:[TableViewTextCell class]]);
    TableViewTextCell* add_account_cell =
        static_cast<TableViewTextCell*>(add_account_cell_);
    EXPECT_NSEQ(add_account_cell.textLabel.text, text);
  }

  // Expects that the cell at `path` is a `TableViewTextCell` whose label’s text
  // is `text`.
  void SelectCell(NSIndexPath* path) {
    [view_controller_ tableView:TableView() didSelectRowAtIndexPath:path];
  }

 private:
  // Signs in kPrimaryIdentity as primary identity.
  void AddPrimaryIdentity() {
    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_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
  std::unique_ptr<TestChromeBrowserState> browser_state_;
};

// Test the view controller when it starts.
TEST_F(AccountMenuViewControllerTest, TestDefaultSetting) {
  EXPECT_EQ(2, TableView().numberOfSections);
  // The secondary account and Add Account...
  EXPECT_EQ(2, [TableView() numberOfRowsInSection:0]);
  // Sign Out
  EXPECT_EQ(1, [TableView() numberOfRowsInSection:1]);
  UITableViewCell* secondary_account_cell =
      GetCell(path_for_secondary_account_);
  EXPECT_TRUE(
      [secondary_account_cell isKindOfClass:[TableViewAccountCell class]]);
  ExpectTextAtPath(
      l10n_util::GetNSString(IDS_IOS_OPTIONS_ACCOUNTS_ADD_ACCOUNT_BUTTON),
      path_for_add_account_);
  ExpectTextAtPath(
      l10n_util::GetNSString(IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_SIGN_OUT_ITEM),
      path_for_sign_out_);
  UIView* table_header_view_ = TableView().tableHeaderView;
  EXPECT_TRUE([table_header_view_ isKindOfClass:[CentralAccountView class]]);
  CentralAccountView* table_header_view =
      static_cast<CentralAccountView*>(table_header_view_);
  EXPECT_EQ(table_header_view.avatarImage, kPrimaryAccountAvatar);
  EXPECT_EQ(table_header_view.name, kPrimaryIdentity.userFullName);
  EXPECT_EQ(table_header_view.email, kPrimaryIdentity.userEmail);
  EXPECT_EQ(table_header_view.managementState.is_managed(), true);
  EXPECT_EQ(table_header_view.managementState.user_level_domain, "acme.com");
}

#pragma mark - Test tapping on the views.

// Tests tapping on the secondary account cell.
TEST_F(AccountMenuViewControllerTest, TestTapSecondaryAccount) {
  OCMExpect([mutator_ accountTappedWithGaiaID:kSecondaryIdentity.gaiaID
                                   targetRect:CGRect()])
      .ignoringNonObjectArgs();
  SelectCell(path_for_secondary_account_);
  EXPECT_EQ(1,
            user_actions_.GetActionCount("Signin_AccountMenu_SelectAccount"));
}

// Tests tapping on the add account cell.
TEST_F(AccountMenuViewControllerTest, TestTapAddAccount) {
  OCMExpect([delegate_ didTapAddAccount]);
  SelectCell(path_for_add_account_);
  EXPECT_EQ(1, user_actions_.GetActionCount("Signin_AccountMenu_AddAccount"));
}

// Tests tapping on the sign-out cell.
TEST_F(AccountMenuViewControllerTest, TestTapSignOut) {
  OCMExpect([delegate_ signOutFromTargetRect:CGRect() callback:nil])
      .ignoringNonObjectArgs();
  SelectCell(path_for_sign_out_);
  EXPECT_EQ(1, user_actions_.GetActionCount("Signin_AccountMenu_Signout"));
}

// Tests tapping on the close button.
TEST_F(AccountMenuViewControllerTest, TestTapClose) {
  UIUserInterfaceIdiom idiom = [[UIDevice currentDevice] userInterfaceIdiom];
  if (idiom == UIUserInterfaceIdiomPad) {
    // There is no close button on ipad.
    return;
  }
  UIBarButtonItem* closeButton =
      view_controller_.navigationItem.rightBarButtonItem;
  OCMExpect([delegate_ viewControllerWantsToBeClosed:view_controller_]);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  [closeButton.target performSelector:closeButton.action];
#pragma clang diagnostic pop
  EXPECT_EQ(1, user_actions_.GetActionCount("Signin_AccountMenu_Close"));
}

// Tests tapping on Manage your account.
TEST_F(AccountMenuViewControllerTest, TestTapManageYourAccount) {
  UIBarButtonItem* ellipsisButton =
      view_controller_.navigationItem.leftBarButtonItem;
  UIMenu* ellipsisMenu = ellipsisButton.menu;
  UIAction* manageYourAccountAction =
      static_cast<UIAction*>(ellipsisMenu.children[0]);
  // Cast the handler block into a form that we can execute
  void (^manageYourAccountHandler)(id obj) =
      [manageYourAccountAction valueForKey:@"handler"];
  // Execute the block
  OCMExpect([delegate_ didTapManageYourGoogleAccount]);
  manageYourAccountHandler(manageYourAccountAction);
  EXPECT_EQ(1,
            user_actions_.GetActionCount("Signin_AccountMenu_ManageAccount"));
}

// Tests tapping on edit account list.
TEST_F(AccountMenuViewControllerTest, TestTapEditAccountsList) {
  UIBarButtonItem* ellipsisButton =
      view_controller_.navigationItem.leftBarButtonItem;
  UIMenu* ellipsisMenu = ellipsisButton.menu;
  UIAction* editAccountsListAction =
      static_cast<UIAction*>(ellipsisMenu.children[1]);
  // Cast the handler block into a form that we can execute
  void (^editAccountsListHandler)(id obj) =
      [editAccountsListAction valueForKey:@"handler"];
  // Execute the block
  OCMExpect([delegate_ didTapEditAccountList]);
  editAccountsListHandler(editAccountsListAction);
  EXPECT_EQ(1,
            user_actions_.GetActionCount("Signin_AccountMenu_EditAccountList"));
}

#pragma mark - AccountMenuConsumer

// Tests tapping on error action button.
TEST_F(AccountMenuViewControllerTest, TestSetError) {
  AccountErrorUIInfo* errorInfo = [[AccountErrorUIInfo alloc]
       initWithErrorType:syncer::SyncService::UserActionableError::
                             kNeedsPassphrase
      userActionableType:AccountErrorUserActionableType::kEnterPassphrase
               messageID:IDS_IOS_ACCOUNT_TABLE_ERROR_ENTER_PASSPHRASE_MESSAGE
           buttonLabelID:IDS_IOS_ACCOUNT_TABLE_ERROR_ENTER_PASSPHRASE_BUTTON];
  data_source_.accountErrorUIInfo = errorInfo;
  [view_controller_ updateErrorSection:errorInfo];
  EXPECT_EQ(3, TableView().numberOfSections);
  // The error section
  EXPECT_EQ(2, [TableView() numberOfRowsInSection:0]);
  // The secondary account and Add Account...
  EXPECT_EQ(2, [TableView() numberOfRowsInSection:0]);
  // Sign Out
  EXPECT_EQ(1, [TableView() numberOfRowsInSection:2]);

  NSIndexPath* path_for_error_message = [NSIndexPath indexPathForRow:0
                                                           inSection:0];
  UITableViewCell* error_message_cell_ = GetCell(path_for_error_message);
  EXPECT_TRUE(
      [error_message_cell_ isKindOfClass:[SettingsImageDetailTextCell class]]);
  SettingsImageDetailTextCell* error_message_cell =
      static_cast<SettingsImageDetailTextCell*>(error_message_cell_);
  EXPECT_NSEQ(error_message_cell.detailTextLabel.text,
              l10n_util::GetNSString(
                  IDS_IOS_ACCOUNT_TABLE_ERROR_ENTER_PASSPHRASE_MESSAGE));
  NSIndexPath* path_for_error_button = [NSIndexPath indexPathForRow:1
                                                          inSection:0];
  ExpectTextAtPath(l10n_util::GetNSString(
                       IDS_IOS_ACCOUNT_TABLE_ERROR_ENTER_PASSPHRASE_BUTTON),
                   path_for_error_button);

  OCMExpect([mutator_ didTapErrorButton]);
  SelectCell(path_for_error_button);
  EXPECT_EQ(1, user_actions_.GetActionCount("Signin_AccountMenu_ErrorButton"));
}

// Tests that adding an account adds an extra row in the secondary account
// section.
TEST_F(AccountMenuViewControllerTest, TestAddAccount) {
  fake_system_identity_manager_->AddIdentity(kSecondaryIdentity2);
  [view_controller_
      updateAccountListWithGaiaIDsToAdd:@[ kSecondaryIdentity2.gaiaID ]
                        gaiaIDsToRemove:@[]];
  EXPECT_EQ(2, TableView().numberOfSections);
  // The secondary accounts and Add Account...
  EXPECT_EQ(3, [TableView() numberOfRowsInSection:0]);
  // Sign Out
  EXPECT_EQ(1, [TableView() numberOfRowsInSection:1]);
}

// Test that removing a secondary account remove a row in the secondary account
// section.
TEST_F(AccountMenuViewControllerTest, TestRemoveAccount) {
  [view_controller_
      updateAccountListWithGaiaIDsToAdd:@[]
                        gaiaIDsToRemove:@[ kSecondaryIdentity.gaiaID ]];
  EXPECT_EQ(2, TableView().numberOfSections);
  // No Secondary account. Just Add Account...
  EXPECT_EQ(1, [TableView() numberOfRowsInSection:0]);
  // Sign Out
  EXPECT_EQ(1, [TableView() numberOfRowsInSection:1]);
}

// Test that updating the primary account has no discernable impact on the view
// controller.
TEST_F(AccountMenuViewControllerTest, TestUpdatePrimaryAccount) {
  [view_controller_ updatePrimaryAccount];
  EXPECT_EQ(2, TableView().numberOfSections);
  // The secondary account and Add Account...
  EXPECT_EQ(2, [TableView() numberOfRowsInSection:0]);
  // Sign Out
  EXPECT_EQ(1, [TableView() numberOfRowsInSection:1]);
}