chromium/ios/chrome/browser/ui/settings/password/password_exporter_unittest.mm

// Copyright 2018 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/settings/password/password_exporter_for_testing.h"

#import "base/strings/utf_string_conversions.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/task_environment.h"
#import "components/password_manager/core/browser/password_form.h"
#import "components/password_manager/core/browser/password_manager_metrics_util.h"
#import "components/password_manager/core/browser/ui/credential_ui_entry.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/app/mock_reauthentication_module.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_mac.h"

@interface FakePasswordSerialzerBridge : NSObject <PasswordSerializerBridge>

// Allows for on demand execution of the block that handles the serialized
// passwords.
- (void)executeHandler;

@end

@implementation FakePasswordSerialzerBridge {
  // Handler processing the serialized passwords.
  void (^_serializedPasswordsHandler)(std::string);
}

- (void)serializePasswords:
            (const std::vector<password_manager::CredentialUIEntry>&)passwords
                   handler:(void (^)(std::string))serializedPasswordsHandler {
  _serializedPasswordsHandler = serializedPasswordsHandler;
}

- (void)executeHandler {
  _serializedPasswordsHandler("test serialized passwords string");
}

@end

@interface FakePasswordFileWriter : NSObject <FileWriterProtocol>

// Allows for on demand execution of the block that should be executed after
// the file has finished writing.
- (void)executeHandler;

// Indicates if the writing of the file was finished successfully or with an
// error.
@property(nonatomic, assign) WriteToURLStatus writingStatus;

// Whether a write operation was attempted.
@property(nonatomic, assign) BOOL writeAttempted;

@end

@implementation FakePasswordFileWriter {
  // Handler executed after the file write operation finishes.
  void (^_writeStatusHandler)(WriteToURLStatus);
}

@synthesize writingStatus = _writingStatus;
@synthesize writeAttempted = _writeAttempted;

- (instancetype)init {
  self = [super init];
  if (self) {
    _writeAttempted = NO;
  }
  return self;
}

- (void)writeData:(NSData*)data
            toURL:(NSURL*)fileURL
          handler:(void (^)(WriteToURLStatus))handler {
  _writeAttempted = YES;
  _writeStatusHandler = handler;
}

- (void)executeHandler {
  _writeStatusHandler(self.writingStatus);
}

@end

namespace {
class PasswordExporterTest : public PlatformTest {
 public:
  PasswordExporterTest() = default;

 protected:
  void SetUp() override {
    PlatformTest::SetUp();
    mock_reauthentication_module_ = [[MockReauthenticationModule alloc] init];
    password_exporter_delegate_ =
        OCMProtocolMock(@protocol(PasswordExporterDelegate));
    password_exporter_ = [[PasswordExporter alloc]
        initWithReauthenticationModule:mock_reauthentication_module_
                              delegate:password_exporter_delegate_];
  }

  std::vector<password_manager::CredentialUIEntry> CreatePasswordList() {
    password_manager::PasswordForm password_form;
    password_form.url = GURL("http://accounts.google.com/a/LoginAuth");
    password_form.username_value = u"[email protected]";
    password_form.password_value = u"test1";

    return {password_manager::CredentialUIEntry(password_form)};
  }

  id password_exporter_delegate_;
  PasswordExporter* password_exporter_;
  MockReauthenticationModule* mock_reauthentication_module_;
  base::test::TaskEnvironment task_environment_;
  base::HistogramTester histogram_tester_;
};

// Tests that when reauthentication is successful, writing the passwords file
// is attempted and a call to show the activity view is made.
TEST_F(PasswordExporterTest, PasswordFileWriteReauthSucceeded) {
  mock_reauthentication_module_.expectedResult =
      ReauthenticationResult::kSuccess;
  FakePasswordFileWriter* fake_password_file_writer =
      [[FakePasswordFileWriter alloc] init];
  fake_password_file_writer.writingStatus = WriteToURLStatus::SUCCESS;
  [password_exporter_ setPasswordFileWriter:fake_password_file_writer];

  OCMExpect([password_exporter_delegate_
      showActivityViewWithActivityItems:[OCMArg any]
                      completionHandler:[OCMArg any]]);

  [password_exporter_ startExportFlow:CreatePasswordList()];

  // Wait for all asynchronous tasks to complete.
  task_environment_.RunUntilIdle();

  EXPECT_TRUE(fake_password_file_writer.writeAttempted);

  // Execute file write completion handler to continue the flow.
  [fake_password_file_writer executeHandler];

  EXPECT_OCMOCK_VERIFY(password_exporter_delegate_);
}

// Tests that if the file writing fails because of not enough disk space
// the appropriate error is displayed and the export operation
// is interrupted.
TEST_F(PasswordExporterTest, WritingFailedOutOfDiskSpace) {
  mock_reauthentication_module_.expectedResult =
      ReauthenticationResult::kSuccess;
  FakePasswordFileWriter* fake_password_file_writer =
      [[FakePasswordFileWriter alloc] init];
  fake_password_file_writer.writingStatus =
      WriteToURLStatus::OUT_OF_DISK_SPACE_ERROR;
  [password_exporter_ setPasswordFileWriter:fake_password_file_writer];

  OCMExpect([password_exporter_delegate_
      showExportErrorAlertWithLocalizedReason:
          l10n_util::GetNSString(
              IDS_IOS_EXPORT_PASSWORDS_OUT_OF_SPACE_ALERT_MESSAGE)]);
  [[password_exporter_delegate_ reject]
      showActivityViewWithActivityItems:[OCMArg any]
                      completionHandler:[OCMArg any]];
  [password_exporter_ startExportFlow:CreatePasswordList()];

  // Wait for all asynchronous tasks to complete.
  task_environment_.RunUntilIdle();

  // Use @try/@catch as -reject raises an exception.
  @try {
    [fake_password_file_writer executeHandler];
    EXPECT_OCMOCK_VERIFY(password_exporter_delegate_);
  } @catch (NSException* exception) {
    // The exception is raised when
    // - showActivityViewWithActivityItems:completionHandler:
    // is invoked. As this should not happen, mark the test as failed.
    GTEST_FAIL();
  }

  // Failure to write the passwords file ends the export operation.
  EXPECT_EQ(ExportState::IDLE, password_exporter_.exportState);
}

// Tests that if a file write fails with an error other than not having
// enough disk space, the appropriate error is displayed and the export
// operation is interrupted.
TEST_F(PasswordExporterTest, WritingFailedUnknownError) {
  mock_reauthentication_module_.expectedResult =
      ReauthenticationResult::kSuccess;
  FakePasswordFileWriter* fake_password_file_writer =
      [[FakePasswordFileWriter alloc] init];
  fake_password_file_writer.writingStatus = WriteToURLStatus::UNKNOWN_ERROR;
  [password_exporter_ setPasswordFileWriter:fake_password_file_writer];

  OCMExpect([password_exporter_delegate_
      showExportErrorAlertWithLocalizedReason:
          l10n_util::GetNSString(
              IDS_IOS_EXPORT_PASSWORDS_UNKNOWN_ERROR_ALERT_MESSAGE)]);
  [[password_exporter_delegate_ reject]
      showActivityViewWithActivityItems:[OCMArg any]
                      completionHandler:[OCMArg any]];
  [password_exporter_ startExportFlow:CreatePasswordList()];

  // Wait for all asynchronous tasks to complete.
  task_environment_.RunUntilIdle();

  // Use @try/@catch as -reject raises an exception.
  @try {
    [fake_password_file_writer executeHandler];
    EXPECT_OCMOCK_VERIFY(password_exporter_delegate_);
  } @catch (NSException* exception) {
    // The exception is raised when
    // - showActivityViewWithActivityItems:completionHandler:
    // is invoked. As this should not happen, mark the test as failed.
    GTEST_FAIL();
  }

  // Failure to write the passwords file ends the export operation.
  EXPECT_EQ(ExportState::IDLE, password_exporter_.exportState);
}

// Tests that when reauthentication fails the export flow is interrupted.
TEST_F(PasswordExporterTest, ExportInterruptedWhenReauthFails) {
  mock_reauthentication_module_.expectedResult =
      ReauthenticationResult::kFailure;
  FakePasswordSerialzerBridge* fake_password_serializer_bridge =
      [[FakePasswordSerialzerBridge alloc] init];
  [password_exporter_
      setPasswordSerializerBridge:fake_password_serializer_bridge];

  FakePasswordFileWriter* fake_password_file_writer =
      [[FakePasswordFileWriter alloc] init];
  [password_exporter_ setPasswordFileWriter:fake_password_file_writer];

  [[password_exporter_delegate_ reject]
      showActivityViewWithActivityItems:[OCMArg any]
                      completionHandler:[OCMArg any]];

  // Use @try/@catch as -reject raises an exception.
  @try {
    [password_exporter_ startExportFlow:CreatePasswordList()];

    // Wait for all asynchronous tasks to complete.
    task_environment_.RunUntilIdle();
    EXPECT_OCMOCK_VERIFY(password_exporter_delegate_);
  } @catch (NSException* exception) {
    // The exception is raised when
    // - showActivityViewWithActivityItems:completionHandler:
    // is invoked. As this should not happen, mark the test as failed.
    GTEST_FAIL();
  }
  // Serializing passwords hasn't finished.
  EXPECT_EQ(ExportState::ONGOING, password_exporter_.exportState);

  [fake_password_serializer_bridge executeHandler];

  // Make sure this test doesn't pass only because file writing hasn't finished
  // yet.
  task_environment_.RunUntilIdle();

  // Serializing passwords has finished, but reauthentication was not
  // successful, so writing the file was not attempted.
  EXPECT_FALSE(fake_password_file_writer.writeAttempted);
  EXPECT_EQ(ExportState::IDLE, password_exporter_.exportState);
}

// Tests that cancelling the export while serialization is still ongoing
// waits for it to finish before cleaning up.
TEST_F(PasswordExporterTest, CancelWaitsForSerializationFinished) {
  mock_reauthentication_module_.expectedResult =
      ReauthenticationResult::kSuccess;
  FakePasswordSerialzerBridge* fake_password_serializer_bridge =
      [[FakePasswordSerialzerBridge alloc] init];
  [password_exporter_
      setPasswordSerializerBridge:fake_password_serializer_bridge];

  FakePasswordFileWriter* fake_password_file_writer =
      [[FakePasswordFileWriter alloc] init];
  [password_exporter_ setPasswordFileWriter:fake_password_file_writer];

  [[password_exporter_delegate_ reject]
      showActivityViewWithActivityItems:[OCMArg any]
                      completionHandler:[OCMArg any]];

  [password_exporter_ startExportFlow:CreatePasswordList()];
  [password_exporter_ cancelExport];
  EXPECT_EQ(ExportState::CANCELLING, password_exporter_.exportState);

  // Use @try/@catch as -reject raises an exception.
  @try {
    [fake_password_serializer_bridge executeHandler];
    // Wait for all asynchronous tasks to complete.
    task_environment_.RunUntilIdle();
    EXPECT_OCMOCK_VERIFY(password_exporter_delegate_);
  } @catch (NSException* exception) {
    // The exception is raised when
    // - showActivityViewWithActivityItems:completionHandler:
    // is invoked. As this should not happen, mark the test as failed.
    GTEST_FAIL();
  }
  EXPECT_FALSE(fake_password_file_writer.writeAttempted);
  EXPECT_EQ(ExportState::IDLE, password_exporter_.exportState);
}

// Tests that if the export is cancelled before writing to file finishes
// successfully the request to show the activity controller isn't made.
TEST_F(PasswordExporterTest, CancelledBeforeWriteToFileFinishesSuccessfully) {
  mock_reauthentication_module_.expectedResult =
      ReauthenticationResult::kSuccess;
  FakePasswordFileWriter* fake_password_file_writer =
      [[FakePasswordFileWriter alloc] init];
  fake_password_file_writer.writingStatus = WriteToURLStatus::SUCCESS;
  [password_exporter_ setPasswordFileWriter:fake_password_file_writer];

  [[password_exporter_delegate_ reject]
      showActivityViewWithActivityItems:[OCMArg any]
                      completionHandler:[OCMArg any]];

  [password_exporter_ startExportFlow:CreatePasswordList()];
  // Wait for all asynchronous tasks to complete.
  task_environment_.RunUntilIdle();
  [password_exporter_ cancelExport];
  EXPECT_EQ(ExportState::CANCELLING, password_exporter_.exportState);

  // Use @try/@catch as -reject raises an exception.
  @try {
    [fake_password_file_writer executeHandler];
    EXPECT_OCMOCK_VERIFY(password_exporter_delegate_);
  } @catch (NSException* exception) {
    // The exception is raised when
    // - showActivityViewWithActivityItems:completionHandler:
    // is invoked. As this should not happen, mark the test as failed.
    GTEST_FAIL();
  }
  EXPECT_EQ(ExportState::IDLE, password_exporter_.exportState);
}

// Tests that if the export is cancelled before writing to file fails
// with an error, the request to show the error alert isn't made.
TEST_F(PasswordExporterTest, CancelledBeforeWriteToFileFails) {
  mock_reauthentication_module_.expectedResult =
      ReauthenticationResult::kSuccess;
  FakePasswordFileWriter* fake_password_file_writer =
      [[FakePasswordFileWriter alloc] init];
  fake_password_file_writer.writingStatus = WriteToURLStatus::UNKNOWN_ERROR;
  [password_exporter_ setPasswordFileWriter:fake_password_file_writer];

  [[password_exporter_delegate_ reject]
      showExportErrorAlertWithLocalizedReason:[OCMArg any]];

  [password_exporter_ startExportFlow:CreatePasswordList()];
  // Wait for all asynchronous tasks to complete.
  task_environment_.RunUntilIdle();
  [password_exporter_ cancelExport];
  EXPECT_EQ(ExportState::CANCELLING, password_exporter_.exportState);

  // Use @try/@catch as -reject raises an exception.
  @try {
    [fake_password_file_writer executeHandler];
    EXPECT_OCMOCK_VERIFY(password_exporter_delegate_);
  } @catch (NSException* exception) {
    // The exception is raised when
    // - showExportErrorAlertWithLocalizedReason:
    // is invoked. As this should not happen, mark the test as failed.
    GTEST_FAIL();
  }
  EXPECT_EQ(ExportState::IDLE, password_exporter_.exportState);
}

}  // namespace