chromium/ios/chrome/browser/ui/save_to_photos/save_to_photos_coordinator_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/ui/save_to_photos/save_to_photos_coordinator.h"

#import <StoreKit/StoreKit.h>

#import "base/apple/foundation_util.h"
#import "base/test/task_environment.h"
#import "components/signin/public/identity_manager/identity_test_environment.h"
#import "ios/chrome/browser/account_picker/ui_bundled/account_picker_configuration.h"
#import "ios/chrome/browser/account_picker/ui_bundled/account_picker_coordinator.h"
#import "ios/chrome/browser/account_picker/ui_bundled/account_picker_coordinator_delegate.h"
#import "ios/chrome/browser/photos/model/photos_service_factory.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/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.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/manage_storage_alert_commands.h"
#import "ios/chrome/browser/shared/public/commands/save_to_photos_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/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_factory.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/identity_manager_factory.h"
#import "ios/chrome/browser/signin/model/identity_test_environment_browser_state_adaptor.h"
#import "ios/chrome/browser/store_kit/model/store_kit_coordinator.h"
#import "ios/chrome/browser/ui/authentication/signin/signin_completion_info.h"
#import "ios/chrome/browser/ui/authentication/signin/signin_constants.h"
#import "ios/chrome/browser/ui/save_to_photos/save_to_photos_coordinator.h"
#import "ios/chrome/browser/ui/save_to_photos/save_to_photos_mediator.h"
#import "ios/chrome/browser/ui/save_to_photos/save_to_photos_mediator_delegate.h"
#import "ios/chrome/test/fakes/fake_ui_view_controller.h"
#import "ios/web/public/test/fakes/fake_web_state.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"

namespace {

// Fake image URL to create the coordinator.
const char kFakeImageUrl[] = "http://example.com/image.png";

}  // namespace

// Unit tests for the SaveToPhotosCoordinator.
class SaveToPhotosCoordinatorTest : public PlatformTest {
 protected:
  void SetUp() final {
    PlatformTest::SetUp();
    TestChromeBrowserState::Builder builder;
    builder.AddTestingFactory(
        IdentityManagerFactory::GetInstance(),
        base::BindRepeating(IdentityTestEnvironmentBrowserStateAdaptor::
                                BuildIdentityManagerForTests));
    browser_state_ = std::move(builder).Build();
    browser_ = std::make_unique<TestBrowser>(browser_state_.get());
    std::unique_ptr<web::FakeWebState> web_state =
        std::make_unique<web::FakeWebState>();
    browser_->GetWebStateList()->InsertWebState(
        std::move(web_state),
        WebStateList::InsertionParams::Automatic().Activate());

    base_view_controller_ = [[FakeUIViewController alloc] init];

    mock_save_to_photos_commands_handler_ =
        OCMStrictProtocolMock(@protocol(SaveToPhotosCommands));
    [browser_->GetCommandDispatcher()
        startDispatchingToTarget:mock_save_to_photos_commands_handler_
                     forProtocol:@protocol(SaveToPhotosCommands)];
    mock_snackbar_commands_handler_ =
        OCMStrictProtocolMock(@protocol(SnackbarCommands));
    [browser_->GetCommandDispatcher()
        startDispatchingToTarget:mock_snackbar_commands_handler_
                     forProtocol:@protocol(SnackbarCommands)];
    mock_application_commands_handler_ =
        OCMStrictProtocolMock(@protocol(ApplicationCommands));
    [browser_->GetCommandDispatcher()
        startDispatchingToTarget:mock_application_commands_handler_
                     forProtocol:@protocol(ApplicationCommands)];
    mock_settings_commands_handler_ =
        OCMStrictProtocolMock(@protocol(SettingsCommands));
    [browser_->GetCommandDispatcher()
        startDispatchingToTarget:mock_settings_commands_handler_
                     forProtocol:@protocol(SettingsCommands)];
    mock_save_to_photos_mediator_ = OCMClassMock([SaveToPhotosMediator class]);
    mock_account_picker_coordinator_ =
        OCMClassMock([AccountPickerCoordinator class]);
  }

  // Set up the mediator stub to ensure the coordinator creates a fake mediator.
  void SetUpMediatorStub() {
    OCMStub([mock_save_to_photos_mediator_ alloc])
        .andReturn(mock_save_to_photos_mediator_);
    OCMStub([mock_save_to_photos_mediator_
                    initWithPhotosService:reinterpret_cast<PhotosService*>(
                                              [OCMArg anyPointer])
                              prefService:reinterpret_cast<PrefService*>(
                                              [OCMArg anyPointer])
                    accountManagerService:reinterpret_cast<
                                              ChromeAccountManagerService*>(
                                              [OCMArg anyPointer])
                          identityManager:reinterpret_cast<
                                              signin::IdentityManager*>(
                                              [OCMArg anyPointer])
                manageStorageAlertHandler:[OCMArg any]
                       applicationHandler:[OCMArg any]])
        .andReturn(mock_save_to_photos_mediator_);
  }

  // Set up the account picker stub to ensure the coordinator creates a fake
  // account picker.
  void SetUpAccountPickerCoordinatorStub(
      AccountPickerConfiguration* configuration,
      FakeUIViewController* view_controller) {
    OCMStub([mock_account_picker_coordinator_ alloc])
        .andReturn(mock_account_picker_coordinator_);
    AccountPickerConfiguration* expected_configuration = configuration;
    OCMStub([mock_account_picker_coordinator_
                initWithBaseViewController:base_view_controller_
                                   browser:browser_.get()
                             configuration:expected_configuration])
        .andReturn(mock_account_picker_coordinator_);
    OCMStub([mock_account_picker_coordinator_ viewController])
        .andReturn(view_controller);
  }

  void TearDown() final {
    [mock_save_to_photos_mediator_ stopMocking];
    [mock_account_picker_coordinator_ stopMocking];
    PlatformTest::TearDown();
  }

  // Create a SaveToPhotosCoordinator to test.
  SaveToPhotosCoordinator* CreateSaveToPhotosCoordinator() {
    return [[SaveToPhotosCoordinator alloc]
        initWithBaseViewController:base_view_controller_
                           browser:browser_.get()
                          imageURL:GURL(kFakeImageUrl)
                          referrer:web::Referrer()
                          webState:GetActiveWebState()];
  }

  // Returns the browser's active web state.
  web::FakeWebState* GetActiveWebState() {
    return static_cast<web::FakeWebState*>(
        browser_->GetWebStateList()->GetActiveWebState());
  }

  base::test::TaskEnvironment task_environment_;
  std::unique_ptr<TestChromeBrowserState> browser_state_;
  std::unique_ptr<TestBrowser> browser_;
  FakeUIViewController* base_view_controller_;

  id mock_save_to_photos_mediator_;
  id mock_account_picker_coordinator_;
  id mock_save_to_photos_commands_handler_;
  id mock_snackbar_commands_handler_;
  id mock_application_commands_handler_;
  id mock_settings_commands_handler_;
};

// Tests that the SaveToPhotosCoordinator creates the mediator when started and
// disconnects it when stopped.
TEST_F(SaveToPhotosCoordinatorTest, StartsAndDisconnectsMediator) {
  SaveToPhotosCoordinator* coordinator = CreateSaveToPhotosCoordinator();

  PhotosService* photosService =
      PhotosServiceFactory::GetForBrowserState(browser_state_.get());
  PrefService* prefService = browser_state_->GetPrefs();
  ChromeAccountManagerService* accountManagerService =
      ChromeAccountManagerServiceFactory::GetForBrowserState(
          browser_state_.get());
  signin::IdentityManager* identityManager =
      IdentityManagerFactory::GetForBrowserState(browser_state_.get());

  OCMExpect([mock_save_to_photos_mediator_ alloc])
      .andReturn(mock_save_to_photos_mediator_);
  ASSERT_TRUE(
      [coordinator.class conformsToProtocol:@protocol(
                                                ManageStorageAlertCommands)]);
  OCMExpect(
      [mock_save_to_photos_mediator_
              initWithPhotosService:photosService
                        prefService:prefService
              accountManagerService:accountManagerService
                    identityManager:identityManager
          manageStorageAlertHandler:static_cast<id<ManageStorageAlertCommands>>(
                                        browser_->GetCommandDispatcher())
                 applicationHandler:static_cast<id<ApplicationCommands>>(
                                        browser_->GetCommandDispatcher())])
      .andReturn(mock_save_to_photos_mediator_);
  ASSERT_TRUE(
      [coordinator conformsToProtocol:@protocol(SaveToPhotosMediatorDelegate)]);
  OCMExpect([mock_save_to_photos_mediator_
      setDelegate:static_cast<id<SaveToPhotosMediatorDelegate>>(coordinator)]);
  OCMExpect([[mock_save_to_photos_mediator_ ignoringNonObjectArgs]
      startWithImageURL:GURL()
               referrer:web::Referrer()
               webState:GetActiveWebState()]);
  [coordinator start];
  EXPECT_OCMOCK_VERIFY(mock_save_to_photos_mediator_);

  OCMExpect([mock_save_to_photos_mediator_ disconnect]);
  [coordinator stop];
  EXPECT_OCMOCK_VERIFY(mock_save_to_photos_mediator_);
}

// Tests that the SaveToPhotosCoordinator presents/dismisses an
// UIAlertController with the expected content when the mediator asks to
// show/hide it.
TEST_F(SaveToPhotosCoordinatorTest, ShowsAndHidesTryAgainOrCancelAlert) {
  SetUpMediatorStub();
  AccountPickerConfiguration* account_picker_configuration =
      [[AccountPickerConfiguration alloc] init];
  FakeUIViewController* account_picker_view_controller =
      [[FakeUIViewController alloc] init];
  SetUpAccountPickerCoordinatorStub(account_picker_configuration,
                                    account_picker_view_controller);

  SaveToPhotosCoordinator* coordinator = CreateSaveToPhotosCoordinator();
  [coordinator start];

  ASSERT_TRUE(
      [coordinator conformsToProtocol:@protocol(SaveToPhotosMediatorDelegate)]);
  [static_cast<id<SaveToPhotosMediatorDelegate>>(coordinator)
      showAccountPickerWithConfiguration:account_picker_configuration
                        selectedIdentity:nil];

  NSString* alertTitle = @"Alert Title";
  NSString* alertMessage = @"Alert message.";
  NSString* tryAgainTitle = @"Try Again";
  ProceduralBlock tryAgainAction = ^{
  };
  NSString* cancelTitle = @"Cancel";
  ProceduralBlock cancelAction = ^{
  };

  EXPECT_EQ(nil, account_picker_view_controller.presentedViewController);

  [static_cast<id<SaveToPhotosMediatorDelegate>>(coordinator)
      showTryAgainOrCancelAlertWithTitle:alertTitle
                                 message:alertMessage
                           tryAgainTitle:tryAgainTitle
                          tryAgainAction:tryAgainAction
                             cancelTitle:cancelTitle
                            cancelAction:cancelAction];

  UIAlertController* alertController = base::apple::ObjCCast<UIAlertController>(
      account_picker_view_controller.presentedViewController);
  EXPECT_NE(nil, alertController);
  EXPECT_NSEQ(alertTitle, alertController.title);
  EXPECT_NSEQ(alertMessage, alertController.message);
  ASSERT_EQ(2U, alertController.actions.count);
  EXPECT_NSEQ(tryAgainTitle, alertController.actions[0].title);
  EXPECT_EQ(UIAlertActionStyleDefault, alertController.actions[0].style);
  EXPECT_NSEQ(alertController.actions[0], alertController.preferredAction);
  EXPECT_NSEQ(cancelTitle, alertController.actions[1].title);
  EXPECT_EQ(UIAlertActionStyleCancel, alertController.actions[1].style);

  [coordinator stop];
}

// Tests that the SaveToPhotosCoordinator creates/destroys an
// StoreKitCoordinator with the expected content when the mediator asks to
// show/hide it.
TEST_F(SaveToPhotosCoordinatorTest, ShowsAndHidesStoreKit) {
  SetUpMediatorStub();

  SaveToPhotosCoordinator* coordinator = CreateSaveToPhotosCoordinator();
  [coordinator start];

  ASSERT_TRUE(
      [coordinator conformsToProtocol:@protocol(SaveToPhotosMediatorDelegate)]);

  NSString* productIdentifier = @"product_identifier";
  NSString* providerToken = @"provider_token";
  NSString* campaignToken = @"campaign_token";

  id mock_store_kit_coordinator = OCMClassMock([StoreKitCoordinator class]);
  OCMExpect([mock_store_kit_coordinator alloc])
      .andReturn(mock_store_kit_coordinator);
  OCMExpect([mock_store_kit_coordinator
                initWithBaseViewController:base_view_controller_
                                   browser:browser_.get()])
      .andReturn(mock_store_kit_coordinator);
  OCMExpect([mock_store_kit_coordinator
      setDelegate:static_cast<id<SaveToPhotosMediatorDelegate>>(coordinator)]);
  NSDictionary* expectedITunesProductParameters = @{
    SKStoreProductParameterITunesItemIdentifier : productIdentifier,
    SKStoreProductParameterProviderToken : providerToken,
    SKStoreProductParameterCampaignToken : campaignToken
  };
  OCMExpect([mock_store_kit_coordinator
      setITunesProductParameters:expectedITunesProductParameters]);
  OCMExpect([base::apple::ObjCCast<StoreKitCoordinator>(
      mock_store_kit_coordinator) start]);

  [static_cast<id<SaveToPhotosMediatorDelegate>>(coordinator)
      showStoreKitWithProductIdentifier:productIdentifier
                          providerToken:providerToken
                          campaignToken:campaignToken];
  EXPECT_OCMOCK_VERIFY(mock_store_kit_coordinator);

  OCMExpect([base::apple::ObjCCast<StoreKitCoordinator>(
      mock_store_kit_coordinator) stop]);
  [static_cast<id<SaveToPhotosMediatorDelegate>>(coordinator) hideStoreKit];
  EXPECT_OCMOCK_VERIFY(mock_store_kit_coordinator);

  [coordinator stop];
}

// Tests that the SaveToPhotosCoordinator presents a snackbar with the expected
// content when the mediator asks to show it.
TEST_F(SaveToPhotosCoordinatorTest, ShowsSnackbar) {
  SetUpMediatorStub();

  SaveToPhotosCoordinator* coordinator = CreateSaveToPhotosCoordinator();
  [coordinator start];

  ASSERT_TRUE(
      [coordinator conformsToProtocol:@protocol(SaveToPhotosMediatorDelegate)]);

  NSString* message = @"Snackbar message";
  NSString* buttonText = @"Button text";
  ProceduralBlock messageAction = ^{
  };
  void (^completionAction)(BOOL) = ^(BOOL) {
  };

  OCMExpect([mock_snackbar_commands_handler_
      showSnackbarWithMessage:message
                   buttonText:buttonText
                messageAction:messageAction
             completionAction:completionAction]);
  [static_cast<id<SaveToPhotosMediatorDelegate>>(coordinator)
      showSnackbarWithMessage:message
                   buttonText:buttonText
                messageAction:messageAction
             completionAction:completionAction];
  EXPECT_OCMOCK_VERIFY(mock_snackbar_commands_handler_);

  [coordinator stop];
}

// Tests that the SaveToPhotosCoordinator uses the -hideSaveToPhotos command
// when the mediator asks.
TEST_F(SaveToPhotosCoordinatorTest, HideSaveToPhotosStopsSaveToPhotos) {
  SetUpMediatorStub();

  SaveToPhotosCoordinator* coordinator = CreateSaveToPhotosCoordinator();
  [coordinator start];

  ASSERT_TRUE(
      [coordinator conformsToProtocol:@protocol(SaveToPhotosMediatorDelegate)]);

  OCMExpect([mock_save_to_photos_commands_handler_ stopSaveToPhotos]);
  [static_cast<id<SaveToPhotosMediatorDelegate>>(coordinator) hideSaveToPhotos];
  EXPECT_OCMOCK_VERIFY(mock_save_to_photos_commands_handler_);

  [coordinator stop];
}

// Tests that the SaveToPhotosCoordinator creates/destroys an
// AccountPickerCoordinator with the expected configuration when the mediator
// asks to show/hide it.
TEST_F(SaveToPhotosCoordinatorTest, ShowsAndHidesAccountPicker) {
  SetUpMediatorStub();
  AccountPickerConfiguration* account_picker_configuration =
      [[AccountPickerConfiguration alloc] init];
  FakeUIViewController* account_picker_view_controller =
      [[FakeUIViewController alloc] init];
  SetUpAccountPickerCoordinatorStub(account_picker_configuration,
                                    account_picker_view_controller);

  SaveToPhotosCoordinator* coordinator = CreateSaveToPhotosCoordinator();
  [coordinator start];

  ASSERT_TRUE(
      [coordinator conformsToProtocol:@protocol(SaveToPhotosMediatorDelegate)]);
  OCMExpect([mock_account_picker_coordinator_
      setDelegate:static_cast<id<SaveToPhotosMediatorDelegate>>(coordinator)]);
  OCMExpect([base::apple::ObjCCast<AccountPickerCoordinator>(
      mock_account_picker_coordinator_) start]);

  [static_cast<id<SaveToPhotosMediatorDelegate>>(coordinator)
      showAccountPickerWithConfiguration:account_picker_configuration
                        selectedIdentity:nil];
  EXPECT_OCMOCK_VERIFY(mock_account_picker_coordinator_);

  OCMExpect([base::apple::ObjCCast<AccountPickerCoordinator>(
      mock_account_picker_coordinator_) stopAnimated:YES]);
  [static_cast<id<SaveToPhotosMediatorDelegate>>(coordinator)
      hideAccountPicker];
  EXPECT_OCMOCK_VERIFY(mock_account_picker_coordinator_);

  [coordinator stop];
}

// Tests that the SaveToPhotosCoordinator shows the Add account view when the
// Account picker requires it.
TEST_F(SaveToPhotosCoordinatorTest, ShowsAddAccount) {
  SetUpMediatorStub();
  AccountPickerConfiguration* account_picker_configuration =
      [[AccountPickerConfiguration alloc] init];
  FakeUIViewController* account_picker_view_controller =
      [[FakeUIViewController alloc] init];
  SetUpAccountPickerCoordinatorStub(account_picker_configuration,
                                    account_picker_view_controller);

  SaveToPhotosCoordinator* coordinator = CreateSaveToPhotosCoordinator();
  [coordinator start];

  ASSERT_TRUE([coordinator
      conformsToProtocol:@protocol(AccountPickerCoordinatorDelegate)]);

  // Expect that a ShowSigninCommand will be dispatched to present the Add
  // account view on top of the account picker view.
  id<SystemIdentity> added_identity = [FakeSystemIdentity fakeIdentity1];
  OCMExpect([mock_application_commands_handler_
              showSignin:[OCMArg checkWithBlock:^BOOL(
                                     ShowSigninCommand* command) {
                if (command) {
                  command.callback(
                      SigninCoordinatorResultSuccess,
                      [SigninCompletionInfo
                          signinCompletionInfoWithIdentity:added_identity]);
                }
                EXPECT_EQ(AuthenticationOperation::kAddAccount,
                          command.operation);
                EXPECT_FALSE(command.identity);
                EXPECT_EQ(signin_metrics::AccessPoint::
                              ACCESS_POINT_SAVE_TO_PHOTOS_IOS,
                          command.accessPoint);
                EXPECT_EQ(
                    signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO,
                    command.promoAction);
                return YES;
              }]
      baseViewController:account_picker_view_controller]);

  // Ask the SaveToPhotosCoordinator to open the Add account view and verify the
  // ShowSigninCommand was dispatched.
  [static_cast<id<AccountPickerCoordinatorDelegate>>(coordinator)
          accountPickerCoordinator:mock_account_picker_coordinator_
      openAddAccountWithCompletion:^(id<SystemIdentity> identity) {
        EXPECT_EQ(added_identity, identity);
      }];
  EXPECT_OCMOCK_VERIFY(mock_application_commands_handler_);

  [coordinator stop];
}