chromium/ios/chrome/browser/drive/model/drive_upload_task_unittest.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/drive/model/drive_upload_task.h"

#import "base/apple/foundation_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/task_environment.h"
#import "ios/chrome/browser/download/model/download_mimetype_util.h"
#import "ios/chrome/browser/drive/model/drive_metrics.h"
#import "ios/chrome/browser/drive/model/test_drive_file_uploader.h"
#import "ios/chrome/browser/drive/model/test_upload_task_observer.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "net/base/url_util.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "url/gurl.h"

using State = UploadTask::State;

// DriveUploadTask unit tests.
class DriveUploadTaskTest : public PlatformTest {
 protected:
  void SetUp() final {
    PlatformTest::SetUp();
    uploading_identity_ = [FakeSystemIdentity fakeIdentity1];
    auto uploader =
        std::make_unique<TestDriveFileUploader>(uploading_identity_);
    uploader_ = uploader.get();
    task_ = std::make_unique<DriveUploadTask>(std::move(uploader));
    observer_ = std::make_unique<TestUploadTaskObserver>();
    task_->AddObserver(observer_.get());
  }

  using TaskEnvironment = base::test::TaskEnvironment;
  TaskEnvironment task_environment_{TaskEnvironment::TimeSource::MOCK_TIME};
  id<SystemIdentity> uploading_identity_;
  raw_ptr<TestDriveFileUploader> uploader_;
  std::unique_ptr<TestUploadTaskObserver> observer_;
  std::unique_ptr<DriveUploadTask> task_;
};

// Tests that upon first starting and later cancelling an upload task, the state
// of the task is correctly updated.
TEST_F(DriveUploadTaskTest, TaskCanBeStartedAndCancelled) {
  EXPECT_EQ(0, task_->GetProgress());
  EXPECT_EQ(State::kNotStarted, task_->GetState());
  EXPECT_EQ(nil, task_->GetError());
  EXPECT_EQ(std::nullopt, task_->GetResponseLink());
  EXPECT_FALSE(task_->IsDone());
  // Starting the task.
  EXPECT_EQ(nullptr, observer_->GetUpdatedUpload());
  task_->Start();
  EXPECT_EQ(task_.get(), observer_->GetUpdatedUpload());
  observer_->ResetUpdatedUpload();
  EXPECT_EQ(State::kInProgress, task_->GetState());
  // Cancelling the task.
  EXPECT_EQ(nullptr, observer_->GetUpdatedUpload());
  task_->Cancel();
  EXPECT_EQ(task_.get(), observer_->GetUpdatedUpload());
  EXPECT_EQ(State::kCancelled, task_->GetState());
  EXPECT_TRUE(task_->IsDone());
}

// Tests that if the destination folder does *NOT* exist, the task will create
// it and upload the file as expected.
TEST_F(DriveUploadTaskTest, CreatesFolderIfNotFound) {
  base::HistogramTester histogram_tester;
  // Set up the uploader to simulate empty search results, a successful folder
  // creation and file upload progress and successful completion.
  uploader_->SetFolderSearchResult({.folder_identifier = nil, .error = nil});
  uploader_->SetFolderCreationResult(
      {.folder_identifier = @"test_folder_identifier", .error = nil});
  const std::vector<DriveFileUploadProgress> progress_elements{
      {0, 100}, {10, 100}, {25, 100}, {50, 100}, {99, 100}, {100, 100},
  };
  uploader_->SetFileUploadProgressElements(progress_elements);
  const char* response_link_str = "https://test_response.file_link";
  const GURL response_link(response_link_str);
  const GURL response_link_with_identifier = net::AppendOrReplaceQueryParameter(
      response_link, "huid",
      base::SysNSStringToUTF8(uploader_->GetIdentity().hashedGaiaID));
  uploader_->SetFileUploadResult(
      { .file_link = @(response_link_str), .error = nil });
  // Set up the task with the name of the parent folder as well as the path,
  // suggested name and MIME type of the file to upload.
  task_->SetDestinationFolderName("test_folder_name");
  const base::FilePath file_path_to_upload{"/test/path/of/file/to/upload"};
  const base::FilePath file_suggested_name{"test_uploaded_file_name"};
  task_->SetFileToUpload(file_path_to_upload, file_suggested_name,
                         "test_mime_type", 42);
  // Test that a folder with appropriate name is searched.
  uploader_->SetSearchFolderQuitClosure(task_environment_.QuitClosure());
  EXPECT_EQ(State::kNotStarted, task_->GetState());
  task_->Start();
  EXPECT_EQ(State::kInProgress, task_->GetState());
  task_environment_.RunUntilQuit();
  EXPECT_NSEQ(@"test_folder_name", uploader_->GetSearchedFolderName());
  // Test that a folder with appropriate name is created.
  uploader_->SetCreateFolderQuitClosure(task_environment_.QuitClosure());
  task_environment_.RunUntilQuit();
  EXPECT_NSEQ(@"test_folder_name", uploader_->GetCreatedFolderName());
  // Test that the file parameters provided earlier are forwarded to the
  // uploader.
  EXPECT_NSEQ(base::apple::FilePathToNSURL(file_path_to_upload),
              uploader_->GetUploadedFileUrl());
  EXPECT_NSEQ(base::apple::FilePathToNSString(file_suggested_name),
              uploader_->GetUploadedFileName());
  EXPECT_NSEQ(@"test_mime_type", uploader_->GetUploadedFileMimeType());
  // Test that the parent folder identifier is the one returned by the uploader.
  EXPECT_NSEQ(@"test_folder_identifier",
              uploader_->GetUploadedFileFolderIdentifier());
  // Test that progress is reported as expected.
  for (const DriveFileUploadProgress& progress : progress_elements) {
    uploader_->SetUploadFileProgressQuitClosure(
        task_environment_.QuitClosure());
    observer_->ResetUpdatedUpload();
    task_environment_.RunUntilQuit();
    EXPECT_EQ(task_.get(), observer_->GetUpdatedUpload());
    const float progress_float =
        static_cast<float>(progress.total_bytes_uploaded) /
        progress.total_bytes_expected_to_upload;
    EXPECT_EQ(progress_float, task_->GetProgress());
  }
  // Test that the result is reported as expected.
  uploader_->SetUploadFileCompletionQuitClosure(
      task_environment_.QuitClosure());
  observer_->ResetUpdatedUpload();
  task_environment_.RunUntilQuit();
  EXPECT_EQ(task_.get(), observer_->GetUpdatedUpload());
  EXPECT_EQ(response_link,
            task_->GetResponseLink(/* add_user_identifier= */ false));
  EXPECT_EQ(response_link_with_identifier,
            task_->GetResponseLink(/* add_user_identifier= */ true));
  EXPECT_NSEQ(nil, task_->GetError());
  EXPECT_EQ(State::kComplete, task_->GetState());
  // Test that expected histograms were recorded.
  histogram_tester.ExpectUniqueSample(kDriveSearchFolderResultSuccessful, true,
                                      1);
  histogram_tester.ExpectUniqueSample(kDriveCreateFolderResultSuccessful, true,
                                      1);
  histogram_tester.ExpectUniqueSample(kDriveFileUploadResultSuccessful, true,
                                      1);
}

// Tests that if the destination folder *DOES* exist, the task will use it as-is
// and upload the file as expected.
TEST_F(DriveUploadTaskTest, UsesExistingFolderIfFound) {
  base::HistogramTester histogram_tester;
  // Set up the uploader to simulate non-empty search result, and file upload
  // progress and successful completion.
  uploader_->SetFolderSearchResult(
      {.folder_identifier = @"test_folder_identifier", .error = nil});
  const std::vector<DriveFileUploadProgress> progress_elements{
      {0, 100}, {10, 100}, {25, 100}, {50, 100}, {99, 100}, {100, 100},
  };
  uploader_->SetFileUploadProgressElements(progress_elements);
  const char* response_link_str = "https://test_response.file_link";
  const GURL response_link(response_link_str);
  const GURL response_link_with_identifier = net::AppendOrReplaceQueryParameter(
      response_link, "huid",
      base::SysNSStringToUTF8(uploader_->GetIdentity().hashedGaiaID));
  uploader_->SetFileUploadResult(
      { .file_link = @(response_link_str), .error = nil });
  // Set up the task with the name of the parent folder as well as the path,
  // suggested name and MIME type of the file to upload.
  task_->SetDestinationFolderName("test_folder_name");
  const base::FilePath file_path_to_upload{"/test/path/of/file/to/upload"};
  const base::FilePath file_suggested_name{"test_uploaded_file_name"};
  task_->SetFileToUpload(file_path_to_upload, file_suggested_name,
                         "test_mime_type", 42);
  // Test that a folder with appropriate name is searched.
  uploader_->SetSearchFolderQuitClosure(task_environment_.QuitClosure());
  EXPECT_EQ(State::kNotStarted, task_->GetState());
  task_->Start();
  EXPECT_EQ(State::kInProgress, task_->GetState());
  task_environment_.RunUntilQuit();
  EXPECT_NSEQ(@"test_folder_name", uploader_->GetSearchedFolderName());
  // Test that the file parameters provided earlier are forwarded to the
  // uploader.
  EXPECT_NSEQ(base::apple::FilePathToNSURL(file_path_to_upload),
              uploader_->GetUploadedFileUrl());
  EXPECT_NSEQ(base::apple::FilePathToNSString(file_suggested_name),
              uploader_->GetUploadedFileName());
  EXPECT_NSEQ(@"test_mime_type", uploader_->GetUploadedFileMimeType());
  // Test that the parent folder identifier is the one returned by the uploader.
  EXPECT_NSEQ(@"test_folder_identifier",
              uploader_->GetUploadedFileFolderIdentifier());
  // Test that progress is reported as expected.
  for (const DriveFileUploadProgress& progress : progress_elements) {
    uploader_->SetUploadFileProgressQuitClosure(
        task_environment_.QuitClosure());
    observer_->ResetUpdatedUpload();
    task_environment_.RunUntilQuit();
    EXPECT_EQ(task_.get(), observer_->GetUpdatedUpload());
    const float progress_float =
        static_cast<float>(progress.total_bytes_uploaded) /
        progress.total_bytes_expected_to_upload;
    EXPECT_EQ(progress_float, task_->GetProgress());
  }
  // Test that the result is reported as expected.
  uploader_->SetUploadFileCompletionQuitClosure(
      task_environment_.QuitClosure());
  observer_->ResetUpdatedUpload();
  task_environment_.RunUntilQuit();
  EXPECT_EQ(task_.get(), observer_->GetUpdatedUpload());
  EXPECT_EQ(response_link,
            task_->GetResponseLink(/* add_user_identifier= */ false));
  EXPECT_EQ(response_link_with_identifier,
            task_->GetResponseLink(/* add_user_identifier= */ true));
  EXPECT_NSEQ(nil, task_->GetError());
  EXPECT_EQ(State::kComplete, task_->GetState());
  // Test that expected histograms were recorded.
  histogram_tester.ExpectUniqueSample(kDriveSearchFolderResultSuccessful, true,
                                      1);
  histogram_tester.ExpectUniqueSample(kDriveCreateFolderResultSuccessful, true,
                                      0);
  histogram_tester.ExpectUniqueSample(kDriveFileUploadResultSuccessful, true,
                                      1);
}

// Tests that if the file upload fails, failure is correctly reported.
TEST_F(DriveUploadTaskTest, ReportsFileUploadFailure) {
  base::HistogramTester histogram_tester;
  // Set up the uploader to simulate non-empty search result, and file upload
  // progress and unsuccessful completion.
  uploader_->SetFolderSearchResult(
      {.folder_identifier = @"test_folder_identifier", .error = nil});
  const std::vector<DriveFileUploadProgress> progress_elements{
      {0, 100},
      {10, 100},
      {25, 100},
      {50, 100},
  };
  uploader_->SetFileUploadProgressElements(progress_elements);
  NSError* file_upload_error = [NSError errorWithDomain:@"test_domain"
                                                   code:400
                                               userInfo:nil];
  uploader_->SetFileUploadResult(
      {.file_link = nil, .error = file_upload_error});
  // Set up the task with the name of the parent folder as well as the path,
  // suggested name and MIME type of the file to upload.
  task_->SetDestinationFolderName("test_folder_name");
  const base::FilePath file_path_to_upload{"/test/path/of/file/to/upload"};
  const base::FilePath file_suggested_name{"test_uploaded_file_name"};
  task_->SetFileToUpload(file_path_to_upload, file_suggested_name,
                         "test_mime_type", 42);
  // Test that a folder with appropriate name is searched.
  uploader_->SetSearchFolderQuitClosure(task_environment_.QuitClosure());
  EXPECT_EQ(State::kNotStarted, task_->GetState());
  task_->Start();
  EXPECT_EQ(State::kInProgress, task_->GetState());
  task_environment_.RunUntilQuit();
  EXPECT_NSEQ(@"test_folder_name", uploader_->GetSearchedFolderName());
  // Test that the file parameters provided earlier are forwarded to the
  // uploader.
  EXPECT_NSEQ(base::apple::FilePathToNSURL(file_path_to_upload),
              uploader_->GetUploadedFileUrl());
  EXPECT_NSEQ(base::apple::FilePathToNSString(file_suggested_name),
              uploader_->GetUploadedFileName());
  EXPECT_NSEQ(@"test_mime_type", uploader_->GetUploadedFileMimeType());
  // Test that the parent folder identifier is the one returned by the uploader.
  EXPECT_NSEQ(@"test_folder_identifier",
              uploader_->GetUploadedFileFolderIdentifier());
  // Test that progress is reported as expected.
  for (const DriveFileUploadProgress& progress : progress_elements) {
    uploader_->SetUploadFileProgressQuitClosure(
        task_environment_.QuitClosure());
    observer_->ResetUpdatedUpload();
    task_environment_.RunUntilQuit();
    EXPECT_EQ(task_.get(), observer_->GetUpdatedUpload());
    const float progress_float =
        static_cast<float>(progress.total_bytes_uploaded) /
        progress.total_bytes_expected_to_upload;
    EXPECT_EQ(progress_float, task_->GetProgress());
  }
  // Test that the result is reported as expected.
  uploader_->SetUploadFileCompletionQuitClosure(
      task_environment_.QuitClosure());
  observer_->ResetUpdatedUpload();
  task_environment_.RunUntilQuit();
  EXPECT_EQ(task_.get(), observer_->GetUpdatedUpload());
  EXPECT_EQ(std::nullopt, task_->GetResponseLink());
  EXPECT_NSEQ(file_upload_error, task_->GetError());
  EXPECT_EQ(State::kFailed, task_->GetState());
  // Test that expected histograms were recorded.
  histogram_tester.ExpectUniqueSample(kDriveSearchFolderResultSuccessful, true,
                                      1);
  histogram_tester.ExpectUniqueSample(kDriveCreateFolderResultSuccessful, true,
                                      0);
  histogram_tester.ExpectUniqueSample(kDriveFileUploadResultSuccessful, false,
                                      1);
  histogram_tester.ExpectUniqueSample(kDriveFileUploadResultErrorCode, 400, 1);
}

// Tests that a task that is destroyed before being started records the expected
// histograms.
TEST_F(DriveUploadTaskTest, TaskNotStartedDestructorRecordsMetrics) {
  base::HistogramTester histogram_tester;
  // Give file to task.
  task_->SetFileToUpload(base::FilePath("file_name.txt"),
                         base::FilePath("file_name.txt"), "text/plain",
                         500 * 1024);  // 500 KB
  // Destroy the upload task.
  task_.reset();
  // Check that all expected histograms are recorded.
  histogram_tester.ExpectUniqueSample(kDriveUploadTaskFinalState,
                                      UploadTaskStateHistogram::kNotStarted, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskNumberOfAttempts) + ".NotStarted", 0, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskMimeType) + ".NotStarted",
      DownloadMimeTypeResult::Text, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskFileSize) + ".NotStarted", 0 /* MB */, 1);
}

// Tests that a task that is destroyed before being started records the expected
// histograms.
TEST_F(DriveUploadTaskTest, TaskInProgressDestructorRecordsMetrics) {
  base::HistogramTester histogram_tester;
  // Give file to task.
  task_->SetFileToUpload(base::FilePath("file_name.pdf"),
                         base::FilePath("file_name.pdf"), "application/pdf",
                         1 * 1024 * 1024);  // 1 MB
  // Start the task.
  task_->Start();
  // Destroy the upload task.
  task_.reset();
  // Check that all expected histograms are recorded.
  histogram_tester.ExpectUniqueSample(kDriveUploadTaskFinalState,
                                      UploadTaskStateHistogram::kCancelled, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskNumberOfAttempts) + ".Cancelled", 1, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskMimeType) + ".Cancelled",
      DownloadMimeTypeResult::AdobePortableDocumentFormat, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskFileSize) + ".Cancelled", 1 /* MB */, 1);
}

// Tests that a task that is destroyed after being started and then cancelled
// records the expected histograms.
TEST_F(DriveUploadTaskTest, TaskCancelledDestructorRecordsMetrics) {
  base::HistogramTester histogram_tester;
  // Give file to task.
  task_->SetFileToUpload(base::FilePath("file_name.pdf"),
                         base::FilePath("file_name.pdf"), "application/pdf",
                         150 * 1024 * 1024);  // 150 MB
  // Start and then cancel the task.
  task_->Start();
  task_->Cancel();
  // Destroy the upload task.
  task_.reset();
  // Check that all expected histograms are recorded.
  histogram_tester.ExpectUniqueSample(kDriveUploadTaskFinalState,
                                      UploadTaskStateHistogram::kCancelled, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskNumberOfAttempts) + ".Cancelled", 1, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskMimeType) + ".Cancelled",
      DownloadMimeTypeResult::AdobePortableDocumentFormat, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskFileSize) + ".Cancelled", 150 /* MB */, 1);
}

TEST_F(DriveUploadTaskTest, TaskCompleteDestructorRecordsMetrics) {
  base::HistogramTester histogram_tester;
  // Give file to task.
  task_->SetFileToUpload(base::FilePath("file_name.pdf"),
                         base::FilePath("file_name.pdf"), "application/pdf",
                         5 * 1024 * 1024 * 1024LL);  // 5 GB
  // Start the task and wait until file upload completion.
  task_->Start();
  uploader_->SetUploadFileCompletionQuitClosure(
      task_environment_.QuitClosure());
  task_environment_.RunUntilQuit();
  // Destroy the upload task.
  task_.reset();
  // Check that all expected histograms are recorded.
  histogram_tester.ExpectUniqueSample(kDriveUploadTaskFinalState,
                                      UploadTaskStateHistogram::kComplete, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskNumberOfAttempts) + ".Complete", 1, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskMimeType) + ".Complete",
      DownloadMimeTypeResult::AdobePortableDocumentFormat, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskFileSize) + ".Complete", 5 * 1024 /* MB */,
      1);
}

TEST_F(DriveUploadTaskTest, TaskFailedDestructorRecordsMetrics) {
  base::HistogramTester histogram_tester;
  // Give file to task.
  task_->SetFileToUpload(base::FilePath("file_name.pdf"),
                         base::FilePath("file_name.pdf"), "application/pdf",
                         1);  // 1 B
  // Start the task and wait until file upload fails.
  task_->Start();
  uploader_->SetUploadFileCompletionQuitClosure(
      task_environment_.QuitClosure());
  NSError* file_upload_error = [NSError errorWithDomain:@"test_domain"
                                                   code:400
                                               userInfo:nil];
  uploader_->SetFileUploadResult(
      {.file_link = nil, .error = file_upload_error});
  task_environment_.RunUntilQuit();
  // Destroy the upload task.
  task_.reset();
  // Check that all expected histograms are recorded.
  histogram_tester.ExpectUniqueSample(kDriveUploadTaskFinalState,
                                      UploadTaskStateHistogram::kFailed, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskNumberOfAttempts) + ".Failed", 1, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskMimeType) + ".Failed",
      DownloadMimeTypeResult::AdobePortableDocumentFormat, 1);
  histogram_tester.ExpectUniqueSample(
      std::string(kDriveUploadTaskFileSize) + ".Failed", 0 /* MB */, 1);
}