chromium/chrome/browser/ash/crosapi/browser_data_migrator_util_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/crosapi/browser_data_migrator_util.h"

#include <sys/stat.h>

#include <map>
#include <optional>
#include <string_view>

#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/strings/stringprintf.h"
#include "base/system/sys_info.h"
#include "base/test/metrics/histogram_tester.h"
#include "chrome/browser/extensions/extension_keeplist_chromeos.h"
#include "chrome/common/chrome_constants.h"
#include "chromeos/ash/components/standalone_browser/fake_migration_progress_tracker.h"
#include "components/sync/base/data_type.h"
#include "components/sync/base/storage_type.h"
#include "components/sync/model/blocking_data_type_store_impl.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/leveldatabase/env_chromium.h"
#include "third_party/leveldatabase/src/include/leveldb/write_batch.h"

namespace ash::browser_data_migrator_util {

namespace {

constexpr char kDownloadsPath[] = "Downloads";
constexpr char kSharedProtoDBPath[] = "shared_proto_db";
constexpr char kBookmarksPath[] = "Bookmarks";
constexpr char kCookiesPath[] = "Cookies";
constexpr char kCachePath[] = "Cache";
constexpr char kCodeCachePath[] = "Code Cache";
constexpr char kCodeCacheUMAName[] = "CodeCache";
constexpr std::string_view kTextFileContent = "Hello, World!";
constexpr char kMoveExtensionId[] = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

// ID of an extension that runs in both Lacros and Ash chrome.
std::string_view GetBothChromesExtensionId() {
  // Any id from the Ash allowlist works for tests. Pick the first
  // element of the allowlist for convenience.
  DCHECK(
      !extensions::GetExtensionsAndAppsRunInOSAndStandaloneBrowser().empty());
  return extensions::GetExtensionsAndAppsRunInOSAndStandaloneBrowser()[0];
}

constexpr syncer::DataType kAshSyncDataType =
    browser_data_migrator_util::kAshOnlySyncDataTypesForLacrosMigration[0];
constexpr syncer::DataType kLacrosSyncDataType = syncer::DataType::WEB_APPS;
static_assert(!base::Contains(
    browser_data_migrator_util::kAshOnlySyncDataTypesForLacrosMigration,
    kLacrosSyncDataType));

struct TargetItemComparator {
  bool operator()(const TargetItem& t1, const TargetItem& t2) const {
    return t1.path < t2.path;
  }
};

std::set<std::string> CollectDictKeys(const base::Value::Dict* dict) {
  std::set<std::string> result;
  if (dict) {
    for (const auto entry : *dict) {
      result.insert(entry.first);
    }
  }
  return result;
}

// Prepare LocalStorage-like LevelDB.
void SetUpLocalStorage(const base::FilePath& path,
                       std::unique_ptr<leveldb::DB>& db) {
  using std::string_literals::operator""s;

  leveldb_env::Options options;
  options.create_if_missing = true;
  leveldb::Status status = leveldb_env::OpenDB(options, path.value(), &db);
  ASSERT_TRUE(status.ok());

  leveldb::WriteBatch batch;
  batch.Put("VERSION", "1");

  std::string keep_extension_id =
      browser_data_migrator_util::kExtensionsAshOnly[0];
  batch.Put("META:chrome-extension://" + keep_extension_id, "meta");
  batch.Put("_chrome-extension://" + keep_extension_id + "\x00key1"s, "value1");

  std::string both_extension_id = std::string(GetBothChromesExtensionId());
  batch.Put("META:chrome-extension://" + both_extension_id, "meta");
  batch.Put("_chrome-extension://" + both_extension_id + "\x00key1"s, "value1");

  std::string move_extension_id = kMoveExtensionId;
  batch.Put("META:chrome-extension://" + move_extension_id, "meta");
  batch.Put("_chrome-extension://" + move_extension_id + "\x00key1"s, "value1");

  leveldb::WriteOptions write_options;
  write_options.sync = true;
  status = db->Write(write_options, &batch);
  ASSERT_TRUE(status.ok());
}

// Prepare StateStore-like LevelDB.
void SetUpStateStore(const base::FilePath& path,
                     std::unique_ptr<leveldb::DB>& db) {
  leveldb_env::Options options;
  options.create_if_missing = true;
  leveldb::Status status = leveldb_env::OpenDB(options, path.value(), &db);
  ASSERT_TRUE(status.ok());

  leveldb::WriteBatch batch;
  std::string keep_extension_id =
      browser_data_migrator_util::kExtensionsAshOnly[0];
  std::string both_extension_id = std::string(GetBothChromesExtensionId());
  batch.Put(keep_extension_id + ".key1", "value1");
  batch.Put(keep_extension_id + ".key2", "value2");
  batch.Put(both_extension_id + ".key1", "value1");
  batch.Put(both_extension_id + ".key2", "value2");
  batch.Put(std::string(kMoveExtensionId) + ".key1", "value1");
  batch.Put(std::string(kMoveExtensionId) + ".key2", "value2");

  leveldb::WriteOptions write_options;
  write_options.sync = true;
  status = db->Write(write_options, &batch);
  ASSERT_TRUE(status.ok());
}

// Prepare Sync Data LevelDB.
void SetUpSyncData(const base::FilePath& path,
                   std::unique_ptr<leveldb::DB>& db) {
  leveldb_env::Options options;
  options.create_if_missing = true;
  leveldb::Status status = leveldb_env::OpenDB(options, path.value(), &db);
  ASSERT_TRUE(status.ok());

  leveldb::WriteBatch batch;
  batch.Put(syncer::FormatDataPrefix(kAshSyncDataType,
                                     syncer::StorageType::kUnspecified) +
                kMoveExtensionId,
            "ash_data");
  batch.Put(syncer::FormatMetaPrefix(kAshSyncDataType,
                                     syncer::StorageType::kUnspecified) +
                kMoveExtensionId,
            "ash_metadata");
  batch.Put(syncer::FormatGlobalMetadataKey(kAshSyncDataType,
                                            syncer::StorageType::kUnspecified),
            "ash_globalmetadata");

  batch.Put(syncer::FormatDataPrefix(kLacrosSyncDataType,
                                     syncer::StorageType::kUnspecified) +
                kMoveExtensionId,
            "lacros_data");
  batch.Put(syncer::FormatMetaPrefix(kLacrosSyncDataType,
                                     syncer::StorageType::kUnspecified) +
                kMoveExtensionId,
            "lacros_metadata");
  batch.Put(syncer::FormatGlobalMetadataKey(kLacrosSyncDataType,
                                            syncer::StorageType::kUnspecified),
            "lacros_globalmetadata");

  leveldb::WriteOptions write_options;
  write_options.sync = true;
  status = db->Write(write_options, &batch);
  ASSERT_TRUE(status.ok());
}

// Return all the key-value pairs in a LevelDB.
std::map<std::string, std::string> ReadLevelDB(const base::FilePath& path) {
  leveldb_env::Options options;
  options.create_if_missing = false;

  std::unique_ptr<leveldb::DB> db;
  leveldb::Status status = leveldb_env::OpenDB(options, path.value(), &db);
  EXPECT_TRUE(status.ok());

  std::map<std::string, std::string> db_map;
  std::unique_ptr<leveldb::Iterator> it(
      db->NewIterator(leveldb::ReadOptions()));
  for (it->SeekToFirst(); it->Valid(); it->Next()) {
    db_map.emplace(it->key().ToString(), it->value().ToString());
  }

  return db_map;
}

}  // namespace

TEST(BrowserDataMigratorUtilTest, NoPathOverlaps) {
  base::span<const char* const> remain_in_ash_paths =
      base::make_span(kRemainInAshDataPaths);
  base::span<const char* const> lacros_data_paths =
      base::make_span(kLacrosDataPaths);
  base::span<const char* const> deletable_paths =
      base::make_span(kDeletablePaths);
  base::span<const char* const> common_data_paths =
      base::make_span(kNeedCopyForMoveDataPaths);

  std::vector<base::span<const char* const>> paths_groups{
      remain_in_ash_paths, lacros_data_paths, deletable_paths,
      common_data_paths};

  auto overlap_checker = [](base::span<const char* const> paths_group_a,
                            base::span<const char* const> paths_group_b) {
    for (const char* path_a : paths_group_a) {
      for (const char* path_b : paths_group_b) {
        if (std::string_view(path_a) == std::string_view(path_b)) {
          LOG(ERROR) << "The following path appears in multiple sets: "
                     << path_a;
          return false;
        }
      }
    }

    return true;
  };

  for (size_t i = 0; i < paths_groups.size() - 1; i++) {
    for (size_t j = i + 1; j < paths_groups.size(); j++) {
      SCOPED_TRACE(base::StringPrintf("i %zu j %zu", i, j));
      EXPECT_TRUE(overlap_checker(paths_groups[i], paths_groups[j]));
    }
  }
}

TEST(BrowserDataMigratorUtilTest, ComputeDirectorySizeWithoutLinks) {
  base::ScopedTempDir dir_1;
  ASSERT_TRUE(dir_1.CreateUniqueTempDir());

  ASSERT_TRUE(base::WriteFile(
      dir_1.GetPath().Append(FILE_PATH_LITERAL("file1")), kTextFileContent));
  ASSERT_TRUE(
      base::CreateDirectory(dir_1.GetPath().Append(FILE_PATH_LITERAL("dir"))));
  ASSERT_TRUE(base::WriteFile(dir_1.GetPath()
                                  .Append(FILE_PATH_LITERAL("dir"))
                                  .Append(FILE_PATH_LITERAL("file2")),
                              kTextFileContent));

  // Check that `ComputeDirectorySizeWithoutLinks` returns the sum of sizes of
  // the two files in the directory.
  EXPECT_EQ(ComputeDirectorySizeWithoutLinks(dir_1.GetPath()),
            static_cast<int>(kTextFileContent.size() * 2));

  base::ScopedTempDir dir_2;
  ASSERT_TRUE(dir_2.CreateUniqueTempDir());
  ASSERT_TRUE(base::WriteFile(
      dir_2.GetPath().Append(FILE_PATH_LITERAL("file3")), kTextFileContent));

  ASSERT_TRUE(CreateSymbolicLink(
      dir_2.GetPath().Append(FILE_PATH_LITERAL("file3")),
      dir_1.GetPath().Append(FILE_PATH_LITERAL("link_to_file3"))));

  // Check that `ComputeDirectorySizeWithoutLinks` does not follow symlinks from
  // `dir_1` to `dir_2`.
  EXPECT_EQ(ComputeDirectorySizeWithoutLinks(dir_1.GetPath()),
            static_cast<int>(kTextFileContent.size() * 2));
}

TEST(BrowserDataMigratorUtilTest, GetUMAItemName) {
  base::FilePath profile_data_dir("/home/chronos/user");

  EXPECT_STREQ(GetUMAItemName(profile_data_dir.Append(kCodeCachePath)).c_str(),
               kCodeCacheUMAName);

  EXPECT_STREQ(
      GetUMAItemName(profile_data_dir.Append(FILE_PATH_LITERAL("abcd")))
          .c_str(),
      kUnknownUMAName);
}

TEST(BrowserDataMigratorUtilTest, GetExtensionKeys) {
  using std::string_literals::operator""s;

  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());

  // Prepare LocalStorage-like LevelDB.
  std::unique_ptr<leveldb::DB> db;
  SetUpLocalStorage(
      scoped_temp_dir.GetPath().Append(FILE_PATH_LITERAL("localstorage")), db);

  ExtensionKeys keys;
  leveldb::Status status =
      GetExtensionKeys(db.get(), LevelDBType::kLocalStorage, &keys);
  EXPECT_TRUE(status.ok());
  db.reset();

  std::string keep_extension_id =
      browser_data_migrator_util::kExtensionsAshOnly[0];
  std::string both_extension_id = std::string(GetBothChromesExtensionId());
  ExtensionKeys expected_keys = {
      {keep_extension_id,
       {
           "META:chrome-extension://" + keep_extension_id,
           "_chrome-extension://" + keep_extension_id + "\x00key1"s,
       }},
      {both_extension_id,
       {
           "META:chrome-extension://" + both_extension_id,
           "_chrome-extension://" + both_extension_id + "\x00key1"s,
       }},
      {kMoveExtensionId,
       {
           "META:chrome-extension://" + std::string(kMoveExtensionId),
           "_chrome-extension://" + std::string(kMoveExtensionId) + "\x00key1"s,
       }},
  };
  EXPECT_EQ(expected_keys, keys);
  keys.clear();

  // Prepare StateStore-like LevelDB.
  SetUpStateStore(
      scoped_temp_dir.GetPath().Append(FILE_PATH_LITERAL("statestore")), db);

  status = GetExtensionKeys(db.get(), LevelDBType::kStateStore, &keys);
  EXPECT_TRUE(status.ok());

  expected_keys = {
      {keep_extension_id,
       {
           keep_extension_id + ".key1",
           keep_extension_id + ".key2",
       }},
      {both_extension_id,
       {
           both_extension_id + ".key1",
           both_extension_id + ".key2",
       }},
      {kMoveExtensionId,
       {
           std::string(kMoveExtensionId) + ".key1",
           std::string(kMoveExtensionId) + ".key2",
       }},
  };
  EXPECT_EQ(expected_keys, keys);
}

TEST(BrowserDataMigratorUtilTest, MigrateLevelDB) {
  using std::string_literals::operator""s;

  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());

  // Prepare LocalStorage-like LevelDB.
  std::unique_ptr<leveldb::DB> db;
  const base::FilePath localstorage_db_path =
      scoped_temp_dir.GetPath().Append(FILE_PATH_LITERAL("localstorage"));
  const base::FilePath localstorage_new_db_path =
      localstorage_db_path.AddExtension(".new");
  SetUpLocalStorage(localstorage_db_path, db);
  db.reset();

  EXPECT_TRUE(MigrateLevelDB(localstorage_db_path, localstorage_new_db_path,
                             LevelDBType::kLocalStorage));

  leveldb_env::Options options;
  options.create_if_missing = false;
  leveldb::Status status =
      leveldb_env::OpenDB(options, localstorage_new_db_path.value(), &db);
  EXPECT_TRUE(status.ok());

  ExtensionKeys keys;
  status = GetExtensionKeys(db.get(), LevelDBType::kLocalStorage, &keys);
  EXPECT_TRUE(status.ok());
  db.reset();

  std::string keep_extension_id =
      browser_data_migrator_util::kExtensionsAshOnly[0];
  std::string both_extension_id = std::string(GetBothChromesExtensionId());
  ExtensionKeys expected_keys = {
      {keep_extension_id,
       {
           "META:chrome-extension://" + keep_extension_id,
           "_chrome-extension://" + keep_extension_id + "\x00key1"s,
       }},
      {both_extension_id,
       {
           "META:chrome-extension://" + both_extension_id,
           "_chrome-extension://" + both_extension_id + "\x00key1"s,
       }},
  };
  EXPECT_EQ(expected_keys, keys);
  keys.clear();

  // Prepare StateStore-like LevelDB.
  const base::FilePath statestore_db_path =
      scoped_temp_dir.GetPath().Append(FILE_PATH_LITERAL("statestore"));
  const base::FilePath statestore_new_db_path =
      statestore_db_path.AddExtension(".new");
  SetUpStateStore(statestore_db_path, db);
  db.reset();

  EXPECT_TRUE(MigrateLevelDB(statestore_db_path, statestore_new_db_path,
                             LevelDBType::kStateStore));

  status = leveldb_env::OpenDB(options, statestore_new_db_path.value(), &db);
  EXPECT_TRUE(status.ok());

  status = GetExtensionKeys(db.get(), LevelDBType::kStateStore, &keys);
  EXPECT_TRUE(status.ok());

  expected_keys = {
      {keep_extension_id,
       {
           keep_extension_id + ".key1",
           keep_extension_id + ".key2",
       }},
      {both_extension_id,
       {
           both_extension_id + ".key1",
           both_extension_id + ".key2",
       }},
  };
  EXPECT_EQ(expected_keys, keys);
}

TEST(BrowserDataMigratorUtilTest, MigrateSyncDataLevelDB) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());

  // Prepare Sync Data LevelDB.
  std::unique_ptr<leveldb::DB> db;
  const base::FilePath db_path =
      scoped_temp_dir.GetPath().Append(FILE_PATH_LITERAL("syncdata"));
  SetUpSyncData(db_path, db);
  db.reset();

  // Migrate Sync Data.
  const base::FilePath ash_db_path = db_path.AddExtension(".ash");
  const base::FilePath lacros_db_path = db_path.AddExtension(".lacros");
  EXPECT_TRUE(MigrateSyncDataLevelDB(db_path, ash_db_path, lacros_db_path));

  // Check resulting Ash database.
  auto ash_db_map = ReadLevelDB(ash_db_path);
  std::map<std::string, std::string> expected_ash_db_map = {
      {syncer::FormatDataPrefix(kAshSyncDataType,
                                syncer::StorageType::kUnspecified) +
           kMoveExtensionId,
       "ash_data"},
      {syncer::FormatMetaPrefix(kAshSyncDataType,
                                syncer::StorageType::kUnspecified) +
           kMoveExtensionId,
       "ash_metadata"},
      {syncer::FormatGlobalMetadataKey(kAshSyncDataType,
                                       syncer::StorageType::kUnspecified),
       "ash_globalmetadata"},
  };
  EXPECT_EQ(expected_ash_db_map, ash_db_map);

  // Check resulting Lacros database.
  auto lacros_db_map = ReadLevelDB(lacros_db_path);
  std::map<std::string, std::string> expected_lacros_db_map = {
      {syncer::FormatDataPrefix(kLacrosSyncDataType,
                                syncer::StorageType::kUnspecified) +
           kMoveExtensionId,
       "lacros_data"},
      {syncer::FormatMetaPrefix(kLacrosSyncDataType,
                                syncer::StorageType::kUnspecified) +
           kMoveExtensionId,
       "lacros_metadata"},
      {syncer::FormatGlobalMetadataKey(kLacrosSyncDataType,
                                       syncer::StorageType::kUnspecified),
       "lacros_globalmetadata"},
  };
  EXPECT_EQ(expected_lacros_db_map, lacros_db_map);
}

TEST(BrowserDataMigratorUtilTest, RecordUserDataSize) {
  base::HistogramTester histogram_tester;

  base::FilePath profile_data_dir("/home/chronos/user");
  // Size in bytes.
  int64_t size = 4 * 1024 * 1024;
  RecordUserDataSize(profile_data_dir.Append(kCodeCachePath), size);

  std::string uma_name =
      std::string(kUserDataStatsRecorderDataSize) + kCodeCacheUMAName;

  histogram_tester.ExpectTotalCount(uma_name, 1);
  histogram_tester.ExpectBucketCount(uma_name, size / 1024 / 1024, 1);
}

TEST(BrowserDataMigratorUtilTest, CopyDirectory) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());

  const base::FilePath copy_from =
      scoped_temp_dir.GetPath().Append("copy_from");

  const char subdirectory[] = "Subdirectory";
  const char data_file[] = "data";
  const char original[] = "original";
  const char symlink[] = "symlink";
  const char sensitive[] = "sensitive";

  // Setup files/directories as described below.
  // |- sensitive/original
  // |- copy_from/
  //     |- data
  //     |- Subdirectory/
  //         |- data
  //         |- Subdirectory/data
  //     |- symlink  /* symlink to original */
  ASSERT_TRUE(
      base::CreateDirectory(scoped_temp_dir.GetPath().Append(sensitive)));
  ASSERT_TRUE(base::WriteFile(
      scoped_temp_dir.GetPath().Append(sensitive).Append(original),
      kTextFileContent));
  ASSERT_TRUE(base::CreateDirectory(copy_from));
  ASSERT_TRUE(base::CreateDirectory(copy_from.Append(subdirectory)));
  ASSERT_TRUE(base::CreateDirectory(
      copy_from.Append(subdirectory).Append(subdirectory)));
  ASSERT_TRUE(base::WriteFile(copy_from.Append(data_file), kTextFileContent));
  ASSERT_TRUE(base::WriteFile(copy_from.Append(subdirectory).Append(data_file),
                              kTextFileContent));
  ASSERT_TRUE(base::WriteFile(
      copy_from.Append(subdirectory).Append(subdirectory).Append(data_file),
      kTextFileContent));
  ASSERT_TRUE(base::CreateSymbolicLink(
      scoped_temp_dir.GetPath().Append(sensitive).Append(original),
      copy_from.Append(symlink)));

  // Test `CopyDirectory()`.
  scoped_refptr<CancelFlag> cancelled = base::MakeRefCounted<CancelFlag>();
  standalone_browser::FakeMigrationProgressTracker progress_tracker;
  const base::FilePath copy_to = scoped_temp_dir.GetPath().Append("copy_to");
  ASSERT_TRUE(
      CopyDirectory(copy_from, copy_to, cancelled.get(), &progress_tracker));

  // Expected `copy_to` structure after `CopyDirectory()`.
  // |- copy_to/
  //     |- data
  //     |- Subdirectory/
  //         |- data
  //         |- Subdirectory/data
  EXPECT_TRUE(base::PathExists(copy_to));
  EXPECT_TRUE(base::PathExists(copy_to.Append(data_file)));
  EXPECT_TRUE(base::PathExists(copy_to.Append(subdirectory).Append(data_file)));
  EXPECT_TRUE(base::PathExists(
      copy_to.Append(subdirectory).Append(subdirectory).Append(data_file)));
  // Make sure that symlink is not copied.
  EXPECT_FALSE(base::PathExists(copy_to.Append(symlink)));
  EXPECT_FALSE(base::PathExists(copy_to.Append(original)));
}

TEST(BrowserDataMigratorUtilTest, EstimatedExtraBytesCreated) {
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  // Set up the directory as below. 'Preferences' and 'Sync Data' are files that
  // are split during the migration. 'shared_proto_db' is one of the
  // need-to-copy items in `kNeedCopyForMoveDataPaths`.
  // |- Preferences
  // |- Sync Data/
  //     |- data
  // |- shared_proto_db
  const std::string pref_text = "*";
  const std::string sync_text(10, '*');
  const std::string spdb_text(100, '*');
  ASSERT_TRUE(CreateDirectory(temp_dir.GetPath().Append(kSyncDataFilePath)));
  ASSERT_TRUE(base::WriteFile(temp_dir.GetPath()
                                  .Append(kSyncDataFilePath)
                                  .Append(FILE_PATH_LITERAL("data")),
                              sync_text));
  ASSERT_TRUE(base::WriteFile(temp_dir.GetPath().Append(kSharedProtoDBPath),
                              spdb_text));
  ASSERT_TRUE(base::WriteFile(
      temp_dir.GetPath().Append(chrome::kPreferencesFilename), pref_text));

  // Expected size should be size(NeedToCopy) + size(Preferences) * 2 +
  // size(Sync Data).
  const int64_t expected_size = 1 * 2 + 10 + 100;
  EXPECT_EQ(EstimatedExtraBytesCreated(temp_dir.GetPath()), expected_size);
}

TEST(BrowserDataMigratorUtilTest, IsAshOnlySyncDataType) {
  // The types that should be recognized as Ash-only are stored in
  // `browser_data_migrator_util::kAshOnlySyncDataTypesForLacrosMigration`.
  // Then any of the following can be suffixed to the type name:
  // - `kDataPrefix` = "-dt-"
  // - `kMetadataPrefix` = "-md-"
  // - `kGlobalMetadataKey` = "-GlobalMetadata"
  // `kDataPrefix` and `kMetadataPrefix` are then followed by an id, while
  // `kGlobalMetadataKey` is not.

  const constexpr char* const kTypes[] = {
      "app_list",
      "arc_package",
      "os_preferences",
      "os_priority_preferences",
      "printers",
      "printers_authorization_servers",
      "wifi_configurations",
      "workspace_desk",
  };

  const constexpr char* const kSuffixes[] = {
      "-dt-",
      "-md-",
  };

  for (const char* const type : kTypes) {
    for (const char* const suffix : kSuffixes) {
      auto key = std::string(type) + std::string(suffix) + "random_id";
      EXPECT_TRUE(IsAshOnlySyncDataType(key));
    }
    auto global_metadata_key = std::string(type) + "-GlobalMetadata";
    EXPECT_TRUE(IsAshOnlySyncDataType(global_metadata_key));
    auto global_metadata_key_with_id =
        std::string(type) + "-GlobalMetadata" + "random_id";
    EXPECT_FALSE(IsAshOnlySyncDataType(global_metadata_key_with_id));
  }

  EXPECT_FALSE(IsAshOnlySyncDataType("random_key"));
}

class BrowserDataMigratorUtilWithTargetsTest : public ::testing::Test {
 public:
  void SetUp() override {
    // Setup profile data directory.
    ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());

    LOG(WARNING) << "Setting up tmp directory.";

    profile_data_dir_ = scoped_temp_dir_.GetPath();

    // Setup files/directories as described below.
    //
    // |- Bookmarks      /* lacros */
    // |- Cookies        /* lacros */
    // |- Downloads/     /* remain in ash */
    //     |- file
    //     |- file 2
    // |- shared_proto_db  /* need to copy */
    // |- Cache          /* deletable */
    // |- Code Cache/    /* deletable */
    //     |- file

    LOG(WARNING) << profile_data_dir_.Append(kBookmarksPath).value();

    // Lacros items.
    ASSERT_TRUE(base::WriteFile(profile_data_dir_.Append(kBookmarksPath),
                                kTextFileContent));
    ASSERT_TRUE(base::WriteFile(profile_data_dir_.Append(kCookiesPath),
                                kTextFileContent));
    // Remain in ash items.
    ASSERT_TRUE(
        base::CreateDirectory(profile_data_dir_.Append(kDownloadsPath)));
    ASSERT_TRUE(base::WriteFile(profile_data_dir_.Append(kDownloadsPath)
                                    .Append(FILE_PATH_LITERAL("file")),
                                kTextFileContent));
    ASSERT_TRUE(base::WriteFile(profile_data_dir_.Append(kDownloadsPath)
                                    .Append(FILE_PATH_LITERAL("file 2")),
                                kTextFileContent));

    // Need to copy items.
    ASSERT_TRUE(base::WriteFile(profile_data_dir_.Append(kSharedProtoDBPath),
                                kTextFileContent));

    // Deletable items.
    ASSERT_TRUE(base::WriteFile(profile_data_dir_.Append(kCachePath),
                                kTextFileContent));
    ASSERT_TRUE(
        base::CreateDirectory(profile_data_dir_.Append(kCodeCachePath)));
    ASSERT_TRUE(base::WriteFile(profile_data_dir_.Append(kCodeCachePath)
                                    .Append(FILE_PATH_LITERAL("file")),
                                kTextFileContent));
  }

  void TearDown() override { EXPECT_TRUE(scoped_temp_dir_.Delete()); }

  base::ScopedTempDir scoped_temp_dir_;
  base::FilePath profile_data_dir_;
};

TEST_F(BrowserDataMigratorUtilWithTargetsTest, GetTargetItems) {
  // Check for lacros data.
  std::vector<TargetItem> expected_lacros_items = {
      {profile_data_dir_.Append(kBookmarksPath), kTextFileContent.size(),
       TargetItem::ItemType::kFile},
      {profile_data_dir_.Append(kCookiesPath), kTextFileContent.size(),
       TargetItem::ItemType::kFile}};
  TargetItems lacros_items =
      GetTargetItems(profile_data_dir_, ItemType::kLacros);
  EXPECT_EQ(lacros_items.total_size,
            static_cast<int>(kTextFileContent.size() * 2));
  ASSERT_EQ(lacros_items.items.size(), expected_lacros_items.size());
  std::sort(lacros_items.items.begin(), lacros_items.items.end(),
            TargetItemComparator());
  for (size_t i = 0; i < lacros_items.items.size(); i++) {
    SCOPED_TRACE(lacros_items.items[i].path.value());
    EXPECT_EQ(lacros_items.items[i], expected_lacros_items[i]);
  }

  // Check for remain in ash data.
  std::vector<TargetItem> expected_remain_in_ash_items = {
      {profile_data_dir_.Append(kDownloadsPath), kTextFileContent.size() * 2,
       TargetItem::ItemType::kDirectory}};
  TargetItems remain_in_ash_items =
      GetTargetItems(profile_data_dir_, ItemType::kRemainInAsh);
  EXPECT_EQ(remain_in_ash_items.total_size,
            static_cast<int>(kTextFileContent.size() * 2));
  ASSERT_EQ(remain_in_ash_items.items.size(),
            expected_remain_in_ash_items.size());
  EXPECT_EQ(remain_in_ash_items.items[0], expected_remain_in_ash_items[0]);

  // Check for items that need copies in lacros.
  std::vector<TargetItem> expected_need_copy_items = {
      {profile_data_dir_.Append(kSharedProtoDBPath), kTextFileContent.size(),
       TargetItem::ItemType::kFile}};
  TargetItems need_copy_items =
      GetTargetItems(profile_data_dir_, ItemType::kNeedCopyForMove);
  EXPECT_EQ(need_copy_items.total_size,
            static_cast<int>(kTextFileContent.size()));
  ASSERT_EQ(need_copy_items.items.size(), expected_need_copy_items.size());
  EXPECT_EQ(need_copy_items.items[0], expected_need_copy_items[0]);

  // Check for deletable items.
  std::vector<TargetItem> expected_deletable_items = {
      {profile_data_dir_.Append(kCachePath), kTextFileContent.size(),
       TargetItem::ItemType::kFile},
      {profile_data_dir_.Append(kCodeCachePath), kTextFileContent.size(),
       TargetItem::ItemType::kDirectory}};
  TargetItems deletable_items =
      GetTargetItems(profile_data_dir_, ItemType::kDeletable);
  std::sort(deletable_items.items.begin(), deletable_items.items.end(),
            TargetItemComparator());
  EXPECT_EQ(deletable_items.total_size,
            static_cast<int>(kTextFileContent.size() * 2));
  ASSERT_EQ(deletable_items.items.size(), expected_deletable_items.size());
  for (size_t i = 0; i < deletable_items.items.size(); i++) {
    SCOPED_TRACE(deletable_items.items[i].path.value());
    EXPECT_EQ(deletable_items.items[i], expected_deletable_items[i]);
  }
}

TEST_F(BrowserDataMigratorUtilWithTargetsTest, DryRunToCollectUMA) {
  base::HistogramTester histogram_tester;

  DryRunToCollectUMA(profile_data_dir_);

  const std::string uma_name_bookmarks =
      std::string(browser_data_migrator_util::kUserDataStatsRecorderDataSize) +
      "Bookmarks";
  const std::string uma_name_cookies =
      std::string(browser_data_migrator_util::kUserDataStatsRecorderDataSize) +
      "Cookies";
  const std::string uma_name_downloads =
      std::string(browser_data_migrator_util::kUserDataStatsRecorderDataSize) +
      "Downloads";
  const std::string uma_name_shared_proto_db =
      std::string(browser_data_migrator_util::kUserDataStatsRecorderDataSize) +
      "SharedProtoDb";
  const std::string uma_name_cache =
      std::string(browser_data_migrator_util::kUserDataStatsRecorderDataSize) +
      "Cache";
  const std::string uma_name_code_cache =
      std::string(browser_data_migrator_util::kUserDataStatsRecorderDataSize) +
      "CodeCache";

  histogram_tester.ExpectBucketCount(uma_name_bookmarks,
                                     kTextFileContent.size() / 1024 / 1024, 1);
  histogram_tester.ExpectBucketCount(uma_name_cookies,
                                     kTextFileContent.size() / 1024 / 1024, 1);
  histogram_tester.ExpectBucketCount(
      uma_name_downloads, kTextFileContent.size() * 2 / 1024 / 1024, 1);
  histogram_tester.ExpectBucketCount(uma_name_shared_proto_db,
                                     kTextFileContent.size() / 1024 / 1024, 1);
  histogram_tester.ExpectBucketCount(uma_name_cache,
                                     kTextFileContent.size() / 1024 / 1024, 1);
  histogram_tester.ExpectBucketCount(uma_name_code_cache,
                                     kTextFileContent.size() / 1024 / 1024, 1);

  histogram_tester.ExpectBucketCount(
      kDryRunLacrosDataSize, kTextFileContent.size() * 2 / 1024 / 1024, 1);
  histogram_tester.ExpectBucketCount(
      kDryRunAshDataSize, kTextFileContent.size() * 2 / 1024 / 1024, 1);
  histogram_tester.ExpectBucketCount(kDryRunCommonDataSize,
                                     kTextFileContent.size() / 1024 / 1024, 1);
  histogram_tester.ExpectBucketCount(
      kDryRunNoCopyDataSize, kTextFileContent.size() * 2 / 1024 / 1024, 1);

  histogram_tester.ExpectTotalCount(kDryRunExtraDiskSpaceOccupiedByMove, 1);
  histogram_tester.ExpectTotalCount(kDryRunFreeDiskSpaceAfterDelete, 1);
  histogram_tester.ExpectTotalCount(kDryRunFreeDiskSpaceAfterMigration, 1);
}

TEST(BrowserDataMigratorUtilTest, UpdatePreferencesKeyByType) {
  const std::string keep_extension_dict_key =
      std::string("extensions.settings.") + kExtensionsAshOnly[0];
  const std::string both_extension_dict_key =
      std::string("extensions.settings.") +
      std::string(GetBothChromesExtensionId());
  const std::string move_extension_dict_key =
      std::string("extensions.settings.") + kMoveExtensionId;

  base::Value::List extension_list;
  extension_list.Append(kExtensionsAshOnly[0]);
  extension_list.Append(GetBothChromesExtensionId());
  extension_list.Append(kMoveExtensionId);
  const std::string extension_list_key = "extensions.pinned_extensions";

  // List of dictionaries instead of list of strings as expected.
  // {"extensions.toolbar": [
  //   { <kExtensionsAshOnly[0]> : "test1"},
  //   { <kExtensionsBothChromes[0]> : "test2"},
  //   { <kMoveExtensionId> : "test3"},
  // ]}
  base::Value::Dict wrong_type_value1;
  wrong_type_value1.Set(kExtensionsAshOnly[0], "test1");
  base::Value::Dict wrong_type_value2;
  wrong_type_value2.Set(GetBothChromesExtensionId(), "test2");
  base::Value::Dict wrong_type_value3;
  wrong_type_value3.Set(kMoveExtensionId, "test3");
  base::Value::List wrong_type_list;
  wrong_type_list.Append(std::move(wrong_type_value1));
  wrong_type_list.Append(std::move(wrong_type_value2));
  wrong_type_list.Append(std::move(wrong_type_value3));
  const std::string wrong_type_key = "extensions.toolbar";

  base::Value::Dict ash_dict;
  ash_dict.SetByDottedPath(keep_extension_dict_key, "test1");
  ash_dict.SetByDottedPath(both_extension_dict_key, "test2");
  ash_dict.SetByDottedPath(move_extension_dict_key, "test3");
  ash_dict.SetByDottedPath(extension_list_key, std::move(extension_list));
  ash_dict.SetByDottedPath(wrong_type_key, std::move(wrong_type_list));
  base::Value::Dict lacros_dict = ash_dict.Clone();

  UpdatePreferencesKeyByType(&ash_dict, "extensions.settings",
                             ChromeType::kAsh);
  UpdatePreferencesKeyByType(&lacros_dict, "extensions.settings",
                             ChromeType::kLacros);

  // Test Ash against expected results.
  base::Value::Dict* d = ash_dict.FindDictByDottedPath("extensions.settings");
  std::set<std::string> expected_keys = {
      kExtensionsAshOnly[0], std::string(GetBothChromesExtensionId())};
  EXPECT_EQ(expected_keys, CollectDictKeys(d));
  // If a type other than string is found in a list, it will be left unchanged.
  base::Value::List* l = ash_dict.FindListByDottedPath(wrong_type_key);
  EXPECT_NE(nullptr, l);
  EXPECT_EQ(3u, l->size());

  // Test Lacros against expected results.
  d = lacros_dict.FindDictByDottedPath("extensions.settings");
  expected_keys = {std::string(GetBothChromesExtensionId()), kMoveExtensionId};
  EXPECT_EQ(expected_keys, CollectDictKeys(d));
  l = lacros_dict.FindListByDottedPath(wrong_type_key);
  EXPECT_NE(nullptr, l);
  EXPECT_EQ(3u, l->size());
}

TEST(BrowserDataMigratorUtilTest, MigratePreferencesContents) {
  const std::string keep_extension_dict_key =
      std::string("extensions.settings.") + kExtensionsAshOnly[0];
  const std::string both_extension_dict_key =
      std::string("extensions.settings.") +
      std::string(GetBothChromesExtensionId());
  const std::string move_extension_dict_key =
      std::string("extensions.settings.") + kMoveExtensionId;

  base::Value::List extension_list;
  extension_list.Append(kExtensionsAshOnly[0]);
  extension_list.Append(GetBothChromesExtensionId());
  extension_list.Append(kMoveExtensionId);
  const std::string extension_list_key = "extensions.pinned_extensions";

  std::string original_contents;
  base::Value::Dict dict;
  dict.SetByDottedPath(kLacrosOnlyPreferencesKeys[0], "test1");
  dict.SetByDottedPath(kAshOnlyPreferencesKeys[0], "test2");
  dict.SetByDottedPath("unrelated.key", "test3");
  dict.SetByDottedPath(keep_extension_dict_key, "test4");
  dict.SetByDottedPath(move_extension_dict_key, "test5");
  dict.SetByDottedPath(extension_list_key, std::move(extension_list));
  base::JSONWriter::Write(dict, &original_contents);

  auto contents = MigratePreferencesContents(original_contents);
  EXPECT_TRUE(contents.has_value());

  std::optional<base::Value> ash_root = base::JSONReader::Read(contents->ash);
  EXPECT_TRUE(ash_root.has_value());
  base::Value::Dict* ash_root_dict = ash_root->GetIfDict();
  EXPECT_NE(nullptr, ash_root_dict);
  EXPECT_EQ(nullptr, ash_root_dict->FindStringByDottedPath(
                         kLacrosOnlyPreferencesKeys[0]));
  EXPECT_EQ("test2",
            *ash_root_dict->FindStringByDottedPath(kAshOnlyPreferencesKeys[0]));
  EXPECT_EQ("test3", *ash_root_dict->FindStringByDottedPath("unrelated.key"));
  EXPECT_EQ("test4",
            *ash_root_dict->FindStringByDottedPath(keep_extension_dict_key));
  EXPECT_EQ(nullptr,
            ash_root_dict->FindStringByDottedPath(move_extension_dict_key));
  base::Value::List* ash_extension_list =
      ash_root_dict->FindListByDottedPath(extension_list_key);
  EXPECT_NE(nullptr, ash_extension_list);
  EXPECT_EQ(2u, ash_extension_list->size());
  EXPECT_EQ(kExtensionsAshOnly[0], (*ash_extension_list)[0].GetString());
  EXPECT_EQ(GetBothChromesExtensionId(), (*ash_extension_list)[1].GetString());

  std::optional<base::Value> lacros_root =
      base::JSONReader::Read(contents->lacros);
  EXPECT_TRUE(lacros_root.has_value());
  base::Value::Dict* lacros_root_dict = lacros_root->GetIfDict();
  EXPECT_NE(nullptr, lacros_root_dict);
  EXPECT_EQ("test1", *lacros_root_dict->FindStringByDottedPath(
                         kLacrosOnlyPreferencesKeys[0]));
  EXPECT_EQ(nullptr, lacros_root_dict->FindStringByDottedPath(
                         kAshOnlyPreferencesKeys[0]));
  EXPECT_EQ("test3",
            *lacros_root_dict->FindStringByDottedPath("unrelated.key"));
  EXPECT_EQ(nullptr,
            lacros_root_dict->FindStringByDottedPath(keep_extension_dict_key));
  EXPECT_EQ("test5",
            *lacros_root_dict->FindStringByDottedPath(move_extension_dict_key));
  base::Value::List* lacros_extension_list =
      lacros_root_dict->FindListByDottedPath(extension_list_key);
  EXPECT_NE(nullptr, lacros_extension_list);
  EXPECT_EQ(2u, lacros_extension_list->size());
  EXPECT_EQ(GetBothChromesExtensionId(),
            (*lacros_extension_list)[0].GetString());
  EXPECT_EQ(kMoveExtensionId, (*lacros_extension_list)[1].GetString());
}

TEST(BrowserDataMigratorUtilTest, MigratePreferences) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());

  const base::FilePath original_preferences_path =
      scoped_temp_dir.GetPath().Append("Preferences.original");
  const base::FilePath ash_preferences_path =
      scoped_temp_dir.GetPath().Append("Preferences.ash");
  const base::FilePath lacros_preferences_path =
      scoped_temp_dir.GetPath().Append("Preferences.lacros");

  std::string original_contents;
  base::Value::Dict dict;
  dict.SetByDottedPath(kLacrosOnlyPreferencesKeys[0], "test1");
  dict.SetByDottedPath(kAshOnlyPreferencesKeys[0], "test2");
  dict.SetByDottedPath("unrelated.key", "test3");
  ASSERT_TRUE(base::JSONWriter::Write(dict, &original_contents));
  ASSERT_TRUE(base::WriteFile(original_preferences_path, original_contents));

  EXPECT_TRUE(browser_data_migrator_util::MigratePreferences(
      original_preferences_path, ash_preferences_path,
      lacros_preferences_path));

  std::string ash_contents;
  std::string lacros_contents;
  EXPECT_TRUE(base::ReadFileToString(ash_preferences_path, &ash_contents));
  EXPECT_TRUE(
      base::ReadFileToString(lacros_preferences_path, &lacros_contents));

  std::optional<base::Value> ash_root = base::JSONReader::Read(ash_contents);
  EXPECT_TRUE(ash_root.has_value());
  base::Value::Dict* ash_root_dict = ash_root->GetIfDict();
  EXPECT_NE(nullptr, ash_root_dict);
  EXPECT_EQ(nullptr, ash_root_dict->FindStringByDottedPath(
                         kLacrosOnlyPreferencesKeys[0]));
  EXPECT_EQ("test2",
            *ash_root_dict->FindStringByDottedPath(kAshOnlyPreferencesKeys[0]));
  EXPECT_EQ("test3", *ash_root_dict->FindStringByDottedPath("unrelated.key"));

  std::optional<base::Value> lacros_root =
      base::JSONReader::Read(lacros_contents);
  EXPECT_TRUE(lacros_root.has_value());
  base::Value::Dict* lacros_root_dict = lacros_root->GetIfDict();
  EXPECT_NE(nullptr, lacros_root_dict);
  EXPECT_EQ(nullptr, lacros_root_dict->FindStringByDottedPath(
                         kAshOnlyPreferencesKeys[0]));
  EXPECT_EQ("test1", *lacros_root_dict->FindStringByDottedPath(
                         kLacrosOnlyPreferencesKeys[0]));
  EXPECT_EQ("test3",
            *lacros_root_dict->FindStringByDottedPath("unrelated.key"));

  std::optional<bool> sync_feature_setup_completed =
      lacros_root_dict->FindBoolByDottedPath(
          kSyncInitialSyncFeatureSetupCompletePrefName);
  ASSERT_NE(std::nullopt, sync_feature_setup_completed);
  EXPECT_TRUE(*sync_feature_setup_completed);
}

}  // namespace ash::browser_data_migrator_util