chromium/chrome/browser/ash/sparky/sparky_delegate_impl_unittest.cc

// 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.

#include "chrome/browser/ash/sparky/sparky_delegate_impl.h"

#include <memory>
#include <optional>

#include "ash/constants/ash_pref_names.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/path_service.h"
#include "base/system/sys_info.h"
#include "base/test/bind.h"
#include "base/test/scoped_running_on_chromeos.h"
#include "base/test/task_environment.h"
#include "base/values.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/dbus/concierge/concierge_client.h"
#include "chromeos/ash/components/dbus/spaced/spaced_client.h"
#include "chromeos/ash/components/disks/disk_mount_manager.h"
#include "chromeos/ash/components/disks/fake_disk_mount_manager.h"
#include "chromeos/ash/components/sparky/sparky_util.h"
#include "components/manta/sparky/sparky_delegate.h"
#include "components/manta/sparky/system_info_delegate.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_task_environment.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/text/bytes_formatting.h"

namespace ash {

namespace {
using SettingsPrivatePrefType = extensions::api::settings_private::PrefType;
using ::testing::UnorderedElementsAre;

MATCHER_P2(File, path, name, "File matches") {
  return arg.path == path && arg.name == name;
}

MATCHER_P5(FileWithSummary,
           path,
           name,
           summary,
           date_modified,
           size_in_bytes,
           "File with summary matches") {
  return arg.path == path && arg.name == name && arg.summary == summary &&
         arg.date_modified == date_modified &&
         arg.size_in_bytes == size_in_bytes;
}

// Get the path to file manager's test data directory.
base::FilePath GetTestDataFilePath(const std::string& file_name) {
  // Get the path to file manager's test data directory.
  base::FilePath source_dir;
  CHECK(base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &source_dir));
  base::FilePath test_data_dir = source_dir.AppendASCII("chrome")
                                     .AppendASCII("test")
                                     .AppendASCII("data")
                                     .AppendASCII("chromeos")
                                     .AppendASCII("file_manager");

  // Return full test data path to the given |file_name|.
  return test_data_dir.Append(base::FilePath::FromUTF8Unsafe(file_name));
}

// Copy a file from the file manager's test data directory to the specified
// target_path.
void AddFile(const std::string& file_name,
             int64_t expected_size,
             base::FilePath target_path) {
  const base::FilePath entry_path = GetTestDataFilePath(file_name);
  target_path = target_path.AppendASCII(file_name);
  ASSERT_TRUE(base::CopyFile(entry_path, target_path))
      << "Copy from " << entry_path.value() << " to " << target_path.value()
      << " failed.";
  // Verify file size.
  base::stat_wrapper_t stat;
  const int res = base::File::Lstat(target_path, &stat);
  ASSERT_FALSE(res < 0) << "Couldn't stat" << target_path.value();
  ASSERT_EQ(expected_size, stat.st_size);
}
}  // namespace

class SparkyDelegateImplTest : public testing::Test {
 public:
  SparkyDelegateImplTest() = default;

  SparkyDelegateImplTest(const SparkyDelegateImplTest&) = delete;
  SparkyDelegateImplTest& operator=(const SparkyDelegateImplTest&) = delete;

  ~SparkyDelegateImplTest() override = default;

  SparkyDelegateImpl* GetSparkyDelegateImpl() {
    return sparky_delegate_impl_.get();
  }

  const base::Value& GetPref(const std::string& setting_id) {
    return profile_->GetPrefs()->GetValue(setting_id);
  }

  void SetBool(const std::string& setting_id, bool bool_val) {
    profile_->GetPrefs()->SetBoolean(setting_id, bool_val);
  }

  void AddToMap(const std::string& pref_name,
                SettingsPrivatePrefType settings_pref_type,
                std::optional<base::Value> value) {
    sparky_delegate_impl_->AddPrefToMap(pref_name, settings_pref_type,
                                        std::move(value));
  }

  SparkyDelegateImpl::SettingsDataList* GetCurrentPrefs() {
    return &sparky_delegate_impl_->current_prefs_;
  }

  // testing::Test:
  void SetUp() override {
    profile_ = std::make_unique<TestingProfile>();
    sparky_delegate_impl_ =
        std::make_unique<SparkyDelegateImpl>(profile_.get());

    // Initialize fake DBus clients.
    ash::ConciergeClient::InitializeFake(/*fake_cicerone_client=*/nullptr);
    ash::SpacedClient::InitializeFake();

    ash::disks::DiskMountManager::InitializeForTesting(
        new ash::disks::FakeDiskMountManager);

    // Create and register MyFiles directory.
    // By emulating chromeos running, GetMyFilesFolderForProfile will return the
    // profile's temporary location instead of $HOME/Downloads.
    base::test::ScopedRunningOnChromeOS running_on_chromeos;
    const base::FilePath my_files_path =
        file_manager::util::GetMyFilesFolderForProfile(profile_.get());
    CHECK(base::CreateDirectory(my_files_path));
    CHECK(storage::ExternalMountPoints::GetSystemInstance()->RegisterFileSystem(
        file_manager::util::GetDownloadsMountPointName(profile_.get()),
        storage::kFileSystemTypeLocal, storage::FileSystemMountOption(),
        my_files_path));

    ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
    sparky_delegate_impl_->SetRootPathForTesting(scoped_temp_dir_.GetPath());

    RunUntilIdle();
  }

  void TearDown() override {
    sparky_delegate_impl_.reset();
    profile_.reset();
    ash::disks::DiskMountManager::Shutdown();
    storage::ExternalMountPoints::GetSystemInstance()->RevokeAllFileSystems();
    ash::SpacedClient::Shutdown();
    ash::ConciergeClient::Shutdown();
  }

  void RunUntilIdle() { task_environment_.RunUntilIdle(); }

 protected:
  base::FilePath Path(const std::string& filename) {
    return scoped_temp_dir_.GetPath().Append(filename);
  }

  void WriteFile(const std::string& filename, const std::string& data) {
    ASSERT_TRUE(base::WriteFile(Path(filename), data));
    ASSERT_TRUE(base::PathExists(Path(filename)));
    RunUntilIdle();
  }

  void CreateDirectory(const std::string& directory_name) {
    ASSERT_TRUE(base::CreateDirectory(Path(directory_name)));
    ASSERT_TRUE(base::PathExists(Path(directory_name)));
    RunUntilIdle();
  }

  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<TestingProfile> profile_;

 private:
  std::unique_ptr<SparkyDelegateImpl> sparky_delegate_impl_;
  base::ScopedTempDir scoped_temp_dir_;
};

TEST_F(SparkyDelegateImplTest, SetSettings) {
  SetBool(prefs::kDarkModeEnabled, false);
  SetBool(prefs::kPowerAdaptiveChargingEnabled, true);
  ASSERT_TRUE(GetSparkyDelegateImpl()->SetSettings(
      std::make_unique<manta::SettingsData>(prefs::kDarkModeEnabled,
                                            manta::PrefType::kBoolean,
                                            base::Value(true))));
  ASSERT_TRUE(GetSparkyDelegateImpl()->SetSettings(
      std::make_unique<manta::SettingsData>(
          prefs::kPowerAdaptiveChargingEnabled, manta::PrefType::kBoolean,
          base::Value(false))));
  const base::Value& dark_mode_val = GetPref(prefs::kDarkModeEnabled);
  const base::Value& adaptive_charging_val =
      GetPref(prefs::kPowerAdaptiveChargingEnabled);
  RunUntilIdle();
  ASSERT_TRUE(dark_mode_val.is_bool());
  ASSERT_TRUE(dark_mode_val.GetBool());
  ASSERT_TRUE(adaptive_charging_val.is_bool());
  ASSERT_FALSE(adaptive_charging_val.GetBool());
}

TEST_F(SparkyDelegateImplTest, AddPrefToMap) {
  AddToMap("bool pref", SettingsPrivatePrefType::kBoolean,
           std::make_optional<base::Value>(true));
  AddToMap("int pref", SettingsPrivatePrefType::kNumber,
           std::make_optional<base::Value>(1));
  AddToMap("double pref", SettingsPrivatePrefType::kNumber,
           std::make_optional<base::Value>(0.5));
  AddToMap("string pref", SettingsPrivatePrefType::kString,
           std::make_optional<base::Value>("my string"));
  RunUntilIdle();
  ASSERT_TRUE(GetCurrentPrefs()->contains("bool pref"));
  ASSERT_TRUE(GetCurrentPrefs()->contains("int pref"));
  ASSERT_TRUE(GetCurrentPrefs()->contains("double pref"));
  ASSERT_TRUE(GetCurrentPrefs()->contains("string pref"));
  ASSERT_EQ(GetCurrentPrefs()->find("bool pref")->second->pref_name,
            "bool pref");
  ASSERT_EQ(GetCurrentPrefs()->find("int pref")->second->pref_name, "int pref");
  ASSERT_EQ(GetCurrentPrefs()->find("double pref")->second->pref_name,
            "double pref");
  ASSERT_EQ(GetCurrentPrefs()->find("string pref")->second->pref_name,
            "string pref");
  ASSERT_EQ(GetCurrentPrefs()->find("bool pref")->second->pref_type,
            manta::PrefType::kBoolean);
  ASSERT_EQ(GetCurrentPrefs()->find("int pref")->second->pref_type,
            manta::PrefType::kInt);
  ASSERT_EQ(GetCurrentPrefs()->find("double pref")->second->pref_type,
            manta::PrefType::kDouble);
  ASSERT_EQ(GetCurrentPrefs()->find("string pref")->second->pref_type,
            manta::PrefType::kString);
  ASSERT_TRUE(GetCurrentPrefs()->find("bool pref")->second->bool_val);
  ASSERT_EQ(GetCurrentPrefs()->find("int pref")->second->int_val, 1);
  ASSERT_EQ(GetCurrentPrefs()->find("double pref")->second->double_val, 0.5);
  ASSERT_EQ(GetCurrentPrefs()->find("string pref")->second->string_val,
            "my string");
}

TEST_F(SparkyDelegateImplTest, ObtainStorageInfo) {
  base::ScopedAllowBlockingForTesting allow_blocking;

  // Get local filesystem storage statistics.
  const base::FilePath mount_path =
      file_manager::util::GetMyFilesFolderForProfile(profile_.get());
  const base::FilePath downloads_path =
      file_manager::util::GetDownloadsFolderForProfile(profile_.get());

  const base::FilePath android_files_path =
      profile_->GetPath().Append("AndroidFiles");
  const base::FilePath android_files_download_path =
      android_files_path.Append("Download");

  // Create directories.
  CHECK(base::CreateDirectory(downloads_path));
  CHECK(base::CreateDirectory(android_files_path));

  // Register android files mount point.
  CHECK(storage::ExternalMountPoints::GetSystemInstance()->RegisterFileSystem(
      file_manager::util::GetAndroidFilesMountPointName(),
      storage::kFileSystemTypeLocal, storage::FileSystemMountOption(),
      android_files_path));

  const int kMountPathBytes = 8092;
  const int kAndroidPathBytes = 15271;
  const int kDownloadsPathBytes = 56758;

  // Add files in MyFiles and Android files.
  AddFile("random.bin", kMountPathBytes, mount_path);          // ~7.9 KB
  AddFile("tall.pdf", kAndroidPathBytes, android_files_path);  // ~14.9 KB
  // Add file in Downloads and simulate bind mount with
  // [android files]/Download.
  AddFile("video.ogv", kDownloadsPathBytes, downloads_path);  // ~55.4 KB

  int64_t total_bytes = base::SysInfo::AmountOfTotalDiskSpace(mount_path);
  int64_t available_bytes = base::SysInfo::AmountOfFreeDiskSpace(mount_path);
  int64_t rounded_total_size = sparky::RoundByteSize(total_bytes);

  std::string available_size =
      base::UTF16ToUTF8(ui::FormatBytes(available_bytes));
  std::string total_size =
      base::UTF16ToUTF8(ui::FormatBytes(rounded_total_size));
  auto quit_closure = task_environment_.QuitClosure();

  GetSparkyDelegateImpl()->ObtainStorageInfo(base::BindLambdaForTesting(
      [&quit_closure, this, available_size,
       total_size](std::unique_ptr<manta::StorageData> storage_data) {
        RunUntilIdle();
        ASSERT_TRUE(storage_data);
        ASSERT_EQ(storage_data->free_bytes, available_size);
        ASSERT_EQ(storage_data->total_bytes, total_size);
        quit_closure.Run();
      }));
}

TEST_F(SparkyDelegateImplTest, GetMyFiles_Simple) {
  WriteFile("cat.txt", "I like cats");
  WriteFile("dog.txt", "dog text");
  WriteFile("turtle.txt", "turtle text");
  auto quit_closure = task_environment_.QuitClosure();
  RunUntilIdle();

  std::string path1 = Path("cat.txt").MaybeAsASCII();
  std::string path2 = Path("dog.txt").MaybeAsASCII();
  std::string path3 = Path("turtle.txt").MaybeAsASCII();

  // Simple test without bytes and with all files allowed.
  GetSparkyDelegateImpl()->GetMyFiles(
      base::BindLambdaForTesting([&quit_closure, path1, path2, path3](
                                     std::vector<manta::FileData> files_data) {
        EXPECT_EQ((int)files_data.size(), 3);
        EXPECT_THAT(
            files_data,
            UnorderedElementsAre(File(path1, "cat.txt"), File(path2, "dog.txt"),
                                 File(path3, "turtle.txt")));
        for (manta::FileData file : files_data) {
          EXPECT_FALSE(file.bytes.has_value());
        }
        quit_closure.Run();
      }),
      false, std::set<std::string>());

  task_environment_.RunUntilQuit();
}

TEST_F(SparkyDelegateImplTest, GetMyFiles_WithFiler) {
  WriteFile("cat.txt", "I like cats");
  WriteFile("dog.txt", "dog text");
  WriteFile("turtle.txt", "turtle text");
  auto quit_closure = task_environment_.QuitClosure();
  RunUntilIdle();

  std::string path1 = Path("cat.txt").MaybeAsASCII();
  std::string path2 = Path("dog.txt").MaybeAsASCII();
  std::string path3 = Path("turtle.txt").MaybeAsASCII();

  GetSparkyDelegateImpl()->GetMyFiles(
      base::BindLambdaForTesting([&quit_closure, path1, path3](
                                     std::vector<manta::FileData> files_data) {
        EXPECT_EQ((int)files_data.size(), 2);
        EXPECT_THAT(files_data,
                    UnorderedElementsAre(File(path1, "cat.txt"),
                                         File(path3, "turtle.txt")));
        quit_closure.Run();
      }),
      false, std::set<std::string>({path1, path3}));

  task_environment_.RunUntilQuit();
}

TEST_F(SparkyDelegateImplTest, GetMyFiles_WithBytes) {
  WriteFile("cat.txt", "I like cats");
  WriteFile("dog.txt", "dog text");
  WriteFile("turtle.txt", "turtle text");
  auto quit_closure = task_environment_.QuitClosure();
  RunUntilIdle();

  std::string path1 = Path("cat.txt").MaybeAsASCII();
  std::string path2 = Path("dog.txt").MaybeAsASCII();
  std::string path3 = Path("turtle.txt").MaybeAsASCII();

  GetSparkyDelegateImpl()->GetMyFiles(
      base::BindLambdaForTesting([&quit_closure, path1, path2, path3](
                                     std::vector<manta::FileData> files_data) {
        EXPECT_EQ((int)files_data.size(), 3);
        EXPECT_THAT(
            files_data,
            UnorderedElementsAre(File(path1, "cat.txt"), File(path2, "dog.txt"),
                                 File(path3, "turtle.txt")));
        for (manta::FileData file : files_data) {
          EXPECT_TRUE(file.bytes.has_value());
        }
        quit_closure.Run();
      }),
      true, std::set<std::string>());

  task_environment_.RunUntilQuit();
}

TEST_F(SparkyDelegateImplTest, FilesSummary) {
  // If no summary data has yet be inserted, then the requested file vector
  // should be empty.
  std::vector<manta::FileData> empty_files_summary =
      GetSparkyDelegateImpl()->GetFileSummaries();
  EXPECT_TRUE(empty_files_summary.empty());

  std::vector<manta::FileData> files_data;
  auto file_1 = manta::FileData("path1", "name1.pdf", "2024");
  file_1.summary = "file 1 summary";
  file_1.size_in_bytes = (int64_t)8234;
  files_data.emplace_back(file_1);

  auto file_2 = manta::FileData("path2", "name2", "2023");
  file_2.summary = "my second file";
  file_2.size_in_bytes = (int64_t)1287;
  files_data.emplace_back(file_2);

  GetSparkyDelegateImpl()->UpdateFileSummaries(files_data);
  std::vector<manta::FileData> files_summary =
      GetSparkyDelegateImpl()->GetFileSummaries();
  EXPECT_EQ(2, (int)files_summary.size());

  EXPECT_THAT(files_summary,
              UnorderedElementsAre(
                  FileWithSummary("path1", "name1.pdf", "file 1 summary",
                                  "2024", (int64_t)8234),
                  FileWithSummary("path2", "name2", "my second file", "2023",
                                  (int64_t)1287)));

  std::vector<manta::FileData> files_data_updated;
  auto file_2_updated = manta::FileData("path2", "name2", "2024");
  file_2_updated.summary = "my second file with extra cool stuff";
  file_2_updated.size_in_bytes = (int64_t)1987;
  files_data_updated.emplace_back(file_2_updated);

  auto file_3 =
      manta::FileData("my/last/path/tree.png", "tree.png", "yesterday");
  file_3.summary = "a photo of a eucalyptus tree";
  file_3.size_in_bytes = (int64_t)456243;
  files_data_updated.emplace_back(file_3);

  GetSparkyDelegateImpl()->UpdateFileSummaries(files_data_updated);
  std::vector<manta::FileData> files_summary_updated =
      GetSparkyDelegateImpl()->GetFileSummaries();

  EXPECT_EQ(3, (int)files_summary_updated.size());

  EXPECT_THAT(files_summary_updated,
              UnorderedElementsAre(
                  FileWithSummary("path1", "name1.pdf", "file 1 summary",
                                  "2024", (int64_t)8234),
                  FileWithSummary("path2", "name2",
                                  "my second file with extra cool stuff",
                                  "2024", (int64_t)1987),
                  FileWithSummary("my/last/path/tree.png", "tree.png",
                                  "a photo of a eucalyptus tree", "yesterday",
                                  (int64_t)456243)));
}

}  // namespace ash