chromium/chrome/browser/ash/phonehub/camera_roll_download_manager_impl_unittest.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/phonehub/camera_roll_download_manager_impl.h"

#include <memory>
#include <optional>
#include <string>

#include "ash/public/cpp/holding_space/holding_space_item.h"
#include "ash/public/cpp/holding_space/holding_space_model.h"
#include "ash/public/cpp/holding_space/holding_space_progress.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_path_watcher.h"
#include "base/files/file_util.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/system/sys_info.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/ui/ash/holding_space/holding_space_keyed_service.h"
#include "chrome/browser/ui/ash/holding_space/holding_space_keyed_service_factory.h"
#include "chrome/browser/ui/ash/holding_space/scoped_test_mount_point.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "chromeos/ash/components/phonehub/camera_roll_download_manager.h"
#include "chromeos/ash/components/phonehub/proto/phonehub_api.pb.h"
#include "chromeos/ash/services/secure_channel/public/mojom/secure_channel_types.mojom.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/scoped_user_manager.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {
namespace phonehub {
namespace {

using CreatePayloadFilesResult =
    CameraRollDownloadManager::CreatePayloadFilesResult;

constexpr char kUserEmail[] = "[email protected]";

std::unique_ptr<TestingProfileManager> CreateTestingProfileManager() {
  auto profile_manager = std::make_unique<TestingProfileManager>(
      TestingBrowserProcess::GetGlobal());
  EXPECT_TRUE(profile_manager->SetUp());
  return profile_manager;
}

}  // namespace

class CameraRollDownloadManagerImplTest : public testing::Test {
 public:
  CameraRollDownloadManagerImplTest()
      : profile_manager_(CreateTestingProfileManager()),
        profile_(profile_manager_->CreateTestingProfile(kUserEmail)),
        user_manager_(new ash::FakeChromeUserManager),
        user_manager_owner_(base::WrapUnique(user_manager_.get())) {
    AccountId account_id(AccountId::FromUserEmail(kUserEmail));
    user_manager_->AddUser(account_id);
    user_manager_->LoginUser(account_id);
    holding_space_keyed_service_ =
        HoldingSpaceKeyedServiceFactory::GetInstance()->GetService(profile_);

    downloads_mount_ =
        holding_space::ScopedTestMountPoint::CreateAndMountDownloads(profile_);

    camera_roll_download_manager_ =
        std::make_unique<CameraRollDownloadManagerImpl>(
            GetDownloadPath(), holding_space_keyed_service_);
  }

  CameraRollDownloadManagerImplTest(const CameraRollDownloadManagerImplTest&) =
      delete;
  CameraRollDownloadManagerImplTest& operator=(
      const CameraRollDownloadManagerImplTest&) = delete;
  ~CameraRollDownloadManagerImplTest() override = default;

  secure_channel::mojom::PayloadFilesPtr CreatePayloadFiles(
      int64_t payload_id,
      const proto::CameraRollItemMetadata& item_metadata) {
    secure_channel::mojom::PayloadFilesPtr files_created;
    base::RunLoop run_loop;
    camera_roll_download_manager()->CreatePayloadFiles(
        payload_id, item_metadata,
        base::BindLambdaForTesting(
            [&](CreatePayloadFilesResult result,
                std::optional<secure_channel::mojom::PayloadFilesPtr>
                    payload_files) {
              EXPECT_EQ(CreatePayloadFilesResult::kSuccess, result);
              EXPECT_TRUE(payload_files.has_value());
              files_created = std::move(payload_files.value());
              run_loop.Quit();
            }));
    run_loop.Run();
    return files_created;
  }

  CreatePayloadFilesResult CreatePayloadFilesAndGetError(
      int64_t payload_id,
      const proto::CameraRollItemMetadata& item_metadata) {
    CreatePayloadFilesResult error;
    base::RunLoop run_loop;
    camera_roll_download_manager()->CreatePayloadFiles(
        payload_id, item_metadata,
        base::BindLambdaForTesting(
            [&](CreatePayloadFilesResult result,
                std::optional<secure_channel::mojom::PayloadFilesPtr>
                    payload_files) {
              EXPECT_NE(CreatePayloadFilesResult::kSuccess, result);
              EXPECT_FALSE(payload_files.has_value());
              error = result;
              run_loop.Quit();
            }));
    run_loop.Run();
    return error;
  }

  const base::FilePath& GetDownloadPath() const {
    return downloads_mount_->GetRootPath();
  }

  const HoldingSpaceModel* GetHoldingSpaceModel() const {
    return holding_space_keyed_service_->model_for_testing();
  }

  CameraRollDownloadManagerImpl* camera_roll_download_manager() {
    return camera_roll_download_manager_.get();
  }

  content::BrowserTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  base::HistogramTester histogram_tester_;

 private:
  std::unique_ptr<TestingProfileManager> profile_manager_;
  const raw_ptr<TestingProfile> profile_;
  const raw_ptr<ash::FakeChromeUserManager, DanglingUntriaged> user_manager_;
  user_manager::ScopedUserManager user_manager_owner_;
  raw_ptr<HoldingSpaceKeyedService> holding_space_keyed_service_;
  std::unique_ptr<holding_space::ScopedTestMountPoint> downloads_mount_;

  std::unique_ptr<CameraRollDownloadManagerImpl> camera_roll_download_manager_;
};

TEST_F(CameraRollDownloadManagerImplTest, CreatePayloadFiles) {
  proto::CameraRollItemMetadata item_metadata;
  item_metadata.set_file_name("IMG_0001.jpeg");
  item_metadata.set_file_size_bytes(1000);

  secure_channel::mojom::PayloadFilesPtr payload_files =
      CreatePayloadFiles(/*payload_id=*/1234, item_metadata);

  EXPECT_TRUE(payload_files->input_file.IsValid());
  EXPECT_TRUE(payload_files->output_file.IsValid());
  EXPECT_TRUE(payload_files->output_file.created());
  EXPECT_TRUE(base::PathExists(GetDownloadPath().Append("IMG_0001.jpeg")));
  const HoldingSpaceItem* holding_space_item = GetHoldingSpaceModel()->GetItem(
      HoldingSpaceItem::Type::kPhoneHubCameraRoll,
      GetDownloadPath().Append("IMG_0001.jpeg"));
  EXPECT_TRUE(holding_space_item != nullptr);
  EXPECT_FALSE(holding_space_item->progress().IsComplete());
  EXPECT_EQ(0, holding_space_item->progress().GetValue());
}

TEST_F(CameraRollDownloadManagerImplTest,
       CreatePayloadFilesWithInvalidFileName) {
  proto::CameraRollItemMetadata item_metadata;
  std::string invalid_file_name = "../../secret/IMG_0001.jpeg";
  item_metadata.set_file_name(invalid_file_name);
  item_metadata.set_file_size_bytes(1000);

  CreatePayloadFilesResult error =
      CreatePayloadFilesAndGetError(/*payload_id=*/1234, item_metadata);

  EXPECT_EQ(CreatePayloadFilesResult::kInvalidFileName, error);
  EXPECT_FALSE(base::PathExists(GetDownloadPath().Append(invalid_file_name)));
  EXPECT_FALSE(GetHoldingSpaceModel()->ContainsItem(
      HoldingSpaceItem::Type::kPhoneHubCameraRoll,
      GetDownloadPath().Append("IMG_0001.jpeg")));
}

TEST_F(CameraRollDownloadManagerImplTest,
       CreatePayloadFilesWithReusedPayloadId) {
  proto::CameraRollItemMetadata item_metadata;
  item_metadata.set_file_name("IMG_0001.jpeg");
  item_metadata.set_file_size_bytes(1000);
  CreatePayloadFiles(/*payload_id=*/1234, item_metadata);

  CreatePayloadFilesResult error =
      CreatePayloadFilesAndGetError(/*payload_id=*/1234, item_metadata);

  EXPECT_EQ(CreatePayloadFilesResult::kPayloadAlreadyExists, error);
}

TEST_F(CameraRollDownloadManagerImplTest,
       CreatePayloadFilesWithInsufficientDiskSpace) {
  proto::CameraRollItemMetadata item_metadata;
  item_metadata.set_file_name("IMG_0001.jpeg");
  int64_t free_disk_space_bytes =
      base::SysInfo::AmountOfFreeDiskSpace(GetDownloadPath());
  item_metadata.set_file_size_bytes(free_disk_space_bytes + 1);

  CreatePayloadFilesResult error =
      CreatePayloadFilesAndGetError(/*payload_id=*/1234, item_metadata);

  EXPECT_EQ(CreatePayloadFilesResult::kInsufficientDiskSpace, error);
  EXPECT_FALSE(base::PathExists(GetDownloadPath().Append("IMG_0001.jpeg")));
  EXPECT_FALSE(GetHoldingSpaceModel()->ContainsItem(
      HoldingSpaceItem::Type::kPhoneHubCameraRoll,
      GetDownloadPath().Append("IMG_0001.jpeg")));
}

TEST_F(CameraRollDownloadManagerImplTest,
       CreatePayloadFilesWithDuplicateNames) {
  proto::CameraRollItemMetadata item_metadata;
  item_metadata.set_file_name("IMG_0001.jpeg");
  item_metadata.set_file_size_bytes(1000);

  // Simulat the same item being downloaded twice.
  CreatePayloadFiles(/*payload_id=*/1234, item_metadata);
  CreatePayloadFiles(/*payload_id=*/-5678, item_metadata);

  EXPECT_TRUE(base::PathExists(GetDownloadPath().Append("IMG_0001.jpeg")));
  EXPECT_TRUE(base::PathExists(GetDownloadPath().Append("IMG_0001 (1).jpeg")));
  EXPECT_TRUE(GetHoldingSpaceModel()->ContainsItem(
      HoldingSpaceItem::Type::kPhoneHubCameraRoll,
      GetDownloadPath().Append("IMG_0001.jpeg")));
  EXPECT_TRUE(GetHoldingSpaceModel()->ContainsItem(
      HoldingSpaceItem::Type::kPhoneHubCameraRoll,
      GetDownloadPath().Append("IMG_0001 (1).jpeg")));
}

TEST_F(CameraRollDownloadManagerImplTest,
       CreatePayloadFilesWithFilePathCollision) {
  proto::CameraRollItemMetadata item_metadata;
  item_metadata.set_file_name("IMG_0001.jpeg");
  item_metadata.set_file_size_bytes(1000);

  // Delete the file for this payload after it has been added to holding space.
  // If CreatePayloadFiles is called for the same item again, it will create the
  // file at the same path. However adding the new payload to holding space will
  // fail because the first payload already exists in the model with the same
  // path.
  CreatePayloadFiles(/*payload_id=*/1234, item_metadata);
  base::FilePath file_path = GetDownloadPath().Append("IMG_0001.jpeg");
  EXPECT_TRUE(GetHoldingSpaceModel()->ContainsItem(
      HoldingSpaceItem::Type::kPhoneHubCameraRoll, file_path));
  EXPECT_TRUE(base::DeleteFile(file_path));

  CreatePayloadFilesResult error =
      CreatePayloadFilesAndGetError(/*payload_id=*/-5678, item_metadata);
  EXPECT_EQ(CreatePayloadFilesResult::kNotUniqueFilePath, error);
}

TEST_F(CameraRollDownloadManagerImplTest, UpdateDownloadProgress) {
  proto::CameraRollItemMetadata item_metadata;
  item_metadata.set_file_name("IMG_0001.jpeg");
  int64_t file_size_bytes = 1024 * 30;  // 30 KB;
  item_metadata.set_file_size_bytes(file_size_bytes);
  CreatePayloadFiles(/*payload_id=*/1234, item_metadata);

  task_environment_.FastForwardBy(base::Seconds(10));
  camera_roll_download_manager()->UpdateDownloadProgress(
      secure_channel::mojom::FileTransferUpdate::New(
          /*payload_id=*/1234,
          secure_channel::mojom::FileTransferStatus::kInProgress,
          /*total_bytes=*/file_size_bytes,
          /*bytes_transferred=*/file_size_bytes / 2));

  const base::FilePath expected_path =
      GetDownloadPath().Append("IMG_0001.jpeg");
  const HoldingSpaceItem* holding_space_item = GetHoldingSpaceModel()->GetItem(
      HoldingSpaceItem::Type::kPhoneHubCameraRoll, expected_path);
  EXPECT_TRUE(holding_space_item != nullptr);
  EXPECT_FALSE(holding_space_item->progress().IsComplete());
  EXPECT_EQ(0.5f, holding_space_item->progress().GetValue());

  task_environment_.FastForwardBy(base::Seconds(5));
  camera_roll_download_manager()->UpdateDownloadProgress(
      secure_channel::mojom::FileTransferUpdate::New(
          /*payload_id=*/1234,
          secure_channel::mojom::FileTransferStatus::kInProgress,
          /*total_bytes=*/file_size_bytes,
          /*bytes_transferred=*/file_size_bytes));
  EXPECT_FALSE(holding_space_item->progress().IsComplete());
  EXPECT_FLOAT_EQ(1, holding_space_item->progress().GetValue().value());

  camera_roll_download_manager()->UpdateDownloadProgress(
      secure_channel::mojom::FileTransferUpdate::New(
          /*payload_id=*/1234,
          secure_channel::mojom::FileTransferStatus::kSuccess,
          /*total_bytes=*/file_size_bytes,
          /*bytes_transferred=*/file_size_bytes));
  EXPECT_TRUE(holding_space_item->progress().IsComplete());
  EXPECT_EQ(1, holding_space_item->progress().GetValue());
  // Expected transfer rate is 30 KB / (10 + 5) s = 2 Kb/s
  histogram_tester_.ExpectUniqueSample(
      "PhoneHub.CameraRoll.DownloadItem.TransferRate", 2,
      /*expected_bucket_count=*/1);
}

TEST_F(CameraRollDownloadManagerImplTest,
       UpdateDownloadProgressWithMultiplePayloads) {
  proto::CameraRollItemMetadata item_metadata_1;
  item_metadata_1.set_file_name("IMG_0001.jpeg");
  item_metadata_1.set_file_size_bytes(1000);
  CreatePayloadFiles(/*payload_id=*/1234, item_metadata_1);
  proto::CameraRollItemMetadata item_metadata_2;
  item_metadata_2.set_file_name("IMG_0002.jpeg");
  item_metadata_2.set_file_size_bytes(2000);
  CreatePayloadFiles(/*payload_id=*/-5678, item_metadata_2);

  camera_roll_download_manager()->UpdateDownloadProgress(
      secure_channel::mojom::FileTransferUpdate::New(
          /*payload_id=*/1234,
          secure_channel::mojom::FileTransferStatus::kSuccess,
          /*total_bytes=*/1000,
          /*bytes_transferred=*/1000));
  camera_roll_download_manager()->UpdateDownloadProgress(
      secure_channel::mojom::FileTransferUpdate::New(
          /*payload_id=*/-5678,
          secure_channel::mojom::FileTransferStatus::kInProgress,
          /*total_bytes=*/2000,
          /*bytes_transferred=*/200));

  const HoldingSpaceItem* holding_space_item_1 =
      GetHoldingSpaceModel()->GetItem(
          HoldingSpaceItem::Type::kPhoneHubCameraRoll,
          GetDownloadPath().Append("IMG_0001.jpeg"));
  EXPECT_TRUE(holding_space_item_1->progress().IsComplete());
  EXPECT_EQ(1, holding_space_item_1->progress().GetValue());
  const HoldingSpaceItem* holding_space_item_2 =
      GetHoldingSpaceModel()->GetItem(
          HoldingSpaceItem::Type::kPhoneHubCameraRoll,
          GetDownloadPath().Append("IMG_0002.jpeg"));
  EXPECT_FALSE(holding_space_item_2->progress().IsComplete());
  EXPECT_EQ(0.1f, holding_space_item_2->progress().GetValue());
}

TEST_F(CameraRollDownloadManagerImplTest,
       UpdateDownloadProgressForCompletedItem) {
  proto::CameraRollItemMetadata item_metadata;
  item_metadata.set_file_name("IMG_0001.jpeg");
  item_metadata.set_file_size_bytes(1000);
  CreatePayloadFiles(/*payload_id=*/1234, item_metadata);

  camera_roll_download_manager()->UpdateDownloadProgress(
      secure_channel::mojom::FileTransferUpdate::New(
          /*payload_id=*/1234,
          secure_channel::mojom::FileTransferStatus::kSuccess,
          /*total_bytes=*/1000,
          /*bytes_transferred=*/1000));
  // Subsequent updates should be ignored once a download is complete.
  camera_roll_download_manager()->UpdateDownloadProgress(
      secure_channel::mojom::FileTransferUpdate::New(
          /*payload_id=*/1234,
          secure_channel::mojom::FileTransferStatus::kInProgress,
          /*total_bytes=*/2000,
          /*bytes_transferred=*/1000));

  const HoldingSpaceItem* holding_space_item = GetHoldingSpaceModel()->GetItem(
      HoldingSpaceItem::Type::kPhoneHubCameraRoll,
      GetDownloadPath().Append("IMG_0001.jpeg"));
  EXPECT_TRUE(holding_space_item->progress().IsComplete());
  EXPECT_EQ(1, holding_space_item->progress().GetValue());
}

TEST_F(CameraRollDownloadManagerImplTest, CleanupFailedItem) {
  proto::CameraRollItemMetadata item_metadata;
  item_metadata.set_file_name("IMG_0001.jpeg");
  item_metadata.set_file_size_bytes(1000);
  CreatePayloadFiles(/*payload_id=*/1234, item_metadata);

  base::FilePath expected_path = GetDownloadPath().Append("IMG_0001.jpeg");
  base::RunLoop delete_file_run_loop;
  base::FilePathWatcher watcher;
  watcher.Watch(expected_path, base::FilePathWatcher::Type::kNonRecursive,
                base::BindLambdaForTesting(
                    [&](const base::FilePath& file_path, bool error) {
                      delete_file_run_loop.Quit();
                    }));
  camera_roll_download_manager()->UpdateDownloadProgress(
      secure_channel::mojom::FileTransferUpdate::New(
          /*payload_id=*/1234,
          secure_channel::mojom::FileTransferStatus::kInProgress,
          /*total_bytes=*/1000,
          /*bytes_transferred=*/200));

  EXPECT_TRUE(GetHoldingSpaceModel()->ContainsItem(
      HoldingSpaceItem::Type::kPhoneHubCameraRoll, expected_path));
  EXPECT_TRUE(base::PathExists(expected_path));

  camera_roll_download_manager()->UpdateDownloadProgress(
      secure_channel::mojom::FileTransferUpdate::New(
          /*payload_id=*/1234,
          secure_channel::mojom::FileTransferStatus::kFailure,
          /*total_bytes=*/1000,
          /*bytes_transferred=*/200));
  delete_file_run_loop.Run();

  EXPECT_FALSE(GetHoldingSpaceModel()->ContainsItem(
      HoldingSpaceItem::Type::kPhoneHubCameraRoll, expected_path));
  EXPECT_FALSE(base::PathExists(expected_path));
}

TEST_F(CameraRollDownloadManagerImplTest, DeleteFile) {
  proto::CameraRollItemMetadata item_metadata;
  item_metadata.set_file_name("IMG_0001.jpeg");
  item_metadata.set_file_size_bytes(1000);
  CreatePayloadFiles(/*payload_id=*/1234, item_metadata);

  base::FilePath expected_path = GetDownloadPath().Append("IMG_0001.jpeg");
  EXPECT_TRUE(base::PathExists(expected_path));
  base::RunLoop delete_file_run_loop;
  base::FilePathWatcher watcher;
  watcher.Watch(expected_path, base::FilePathWatcher::Type::kNonRecursive,
                base::BindLambdaForTesting(
                    [&](const base::FilePath& file_path, bool error) {
                      delete_file_run_loop.Quit();
                    }));

  camera_roll_download_manager()->DeleteFile(/*payload_id=*/1234);
  delete_file_run_loop.Run();

  EXPECT_FALSE(base::PathExists(expected_path));
}

}  // namespace phonehub
}  // namespace ash