chromium/chrome/browser/enterprise/connectors/analysis/source_destination_matcher_ash_unittest.cc

// Copyright 2022 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/enterprise/connectors/analysis/source_destination_matcher_ash.h"

#include <map>
#include <set>
#include <string>
#include <vector>

#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/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/bind.h"
#include "chrome/browser/ash/file_manager/volume_manager.h"
#include "chrome/browser/ash/file_manager/volume_manager_factory.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/disks/disk_mount_manager.h"
#include "chromeos/ash/components/disks/fake_disk_mount_manager.h"
#include "components/enterprise/common/proto/connectors.pb.h"
#include "content/public/test/browser_task_environment.h"
#include "storage/browser/file_system/file_system_url.h"
#include "storage/common/file_system/file_system_types.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace enterprise_connectors {

namespace {

struct VolumeInfo {
  file_manager::VolumeType type;
  std::optional<guest_os::VmType> vm_type;
  const char* fs_config_string;
};

base::FilePath GetBasePathForVolume(base::FilePath path,
                                    const VolumeInfo& volume_info) {
  base::FilePath volume_path =
      path.Append(base::NumberToString(volume_info.type));
  if (volume_info.vm_type.has_value())
    volume_path = volume_path.Append(
        "_" + base::NumberToString(volume_info.vm_type.value()));
  else
    volume_path = volume_path.Append("_noVmType");

  return volume_path;
}

constexpr std::array kVolumeInfos{
    VolumeInfo{file_manager::VOLUME_TYPE_TESTING, std::nullopt, "TESTING"},
    VolumeInfo{file_manager::VOLUME_TYPE_GOOGLE_DRIVE, std::nullopt,
               "GOOGLE_DRIVE"},
    VolumeInfo{file_manager::VOLUME_TYPE_DOWNLOADS_DIRECTORY, std::nullopt,
               "MY_FILES"},
    VolumeInfo{file_manager::VOLUME_TYPE_REMOVABLE_DISK_PARTITION, std::nullopt,
               "REMOVABLE"},
    VolumeInfo{file_manager::VOLUME_TYPE_MOUNTED_ARCHIVE_FILE, std::nullopt,
               "TESTING"},
    VolumeInfo{file_manager::VOLUME_TYPE_PROVIDED, std::nullopt, "PROVIDED"},
    VolumeInfo{file_manager::VOLUME_TYPE_MTP, std::nullopt,
               "DEVICE_MEDIA_STORAGE"},
    VolumeInfo{file_manager::VOLUME_TYPE_MEDIA_VIEW, std::nullopt, "ARC"},
    VolumeInfo{file_manager::VOLUME_TYPE_CROSTINI, std::nullopt, "CROSTINI"},
    VolumeInfo{file_manager::VOLUME_TYPE_ANDROID_FILES, std::nullopt, "ARC"},
    VolumeInfo{file_manager::VOLUME_TYPE_DOCUMENTS_PROVIDER, std::nullopt,
               "ARC"},
    VolumeInfo{file_manager::VOLUME_TYPE_SMB, std::nullopt, "SMB"},
    VolumeInfo{file_manager::VOLUME_TYPE_SYSTEM_INTERNAL, std::nullopt,
               "UNKNOWN"},
    VolumeInfo{file_manager::VOLUME_TYPE_GUEST_OS, guest_os::VmType::TERMINA,
               "CROSTINI"},
    VolumeInfo{file_manager::VOLUME_TYPE_GUEST_OS, guest_os::VmType::PLUGIN_VM,
               "PLUGIN_VM"},
    VolumeInfo{file_manager::VOLUME_TYPE_GUEST_OS, guest_os::VmType::BOREALIS,
               "BOREALIS"},
    VolumeInfo{file_manager::VOLUME_TYPE_GUEST_OS, guest_os::VmType::BRUSCHETTA,
               "BRUSCHETTA"},
    VolumeInfo{file_manager::VOLUME_TYPE_GUEST_OS, guest_os::VmType::UNKNOWN,
               "UNKNOWN_VM"},
    VolumeInfo{file_manager::VOLUME_TYPE_GUEST_OS, std::nullopt, "UNKNOWN_VM"},
    VolumeInfo{file_manager::VOLUME_TYPE_GUEST_OS, guest_os::VmType::ARCVM,
               "ARC"}};

void AddVolumes(Profile* profile, base::FilePath path) {
  file_manager::VolumeManager* const volume_manager =
      file_manager::VolumeManager::Get(profile);
  for (const VolumeInfo& volume_info : kVolumeInfos) {
    base::FilePath volume_path = GetBasePathForVolume(path, volume_info);
    EXPECT_TRUE(base::CreateDirectory(volume_path));

    if (volume_info.type == file_manager::VOLUME_TYPE_MOUNTED_ARCHIVE_FILE) {
      // A mounted archive needs a proper source path to be mounted correctly.
      base::FilePath source_path =
          GetBasePathForVolume(path, kVolumeInfos[0]).Append("source.zip");
      volume_manager->AddVolumeForTesting(
          file_manager::Volume::CreateForTesting(
              volume_path, volume_info.type, volume_info.vm_type, source_path));
    } else {
      volume_manager->AddVolumeForTesting(
          file_manager::Volume::CreateForTesting(volume_path, volume_info.type,
                                                 volume_info.vm_type));
    }
  }
}

class BaseTest : public testing::Test {
 public:
  BaseTest() : profile_manager_(TestingBrowserProcess::GetGlobal()) {
    EXPECT_TRUE(profile_manager_.SetUp());
    profile_ = profile_manager_.CreateTestingProfile("test-user");

    file_manager::VolumeManagerFactory::GetInstance()->SetTestingFactory(
        profile_.get(),
        base::BindLambdaForTesting([](content::BrowserContext* context) {
          return std::unique_ptr<KeyedService>(
              std::make_unique<file_manager::VolumeManager>(
                  Profile::FromBrowserContext(context), nullptr, nullptr,
                  ash::disks::DiskMountManager::GetInstance(), nullptr,
                  file_manager::VolumeManager::GetMtpStorageInfoCallback()));
        }));

    // Takes ownership of `disk_mount_manager_`, but Shutdown() must be called.
    ash::disks::DiskMountManager::InitializeForTesting(
        new ash::disks::FakeDiskMountManager);

    // Register volumes.
    EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
    AddVolumes(profile_, temp_dir_.GetPath());
  }

  ~BaseTest() override {
    profile_manager_.DeleteAllTestingProfiles();
    ash::disks::DiskMountManager::Shutdown();
  }

  storage::FileSystemURL PathToFileSystemURL(base::FilePath path) {
    return storage::FileSystemURL::CreateForTest(
        kTestStorageKey, storage::kFileSystemTypeLocal, path);
  }

  storage::FileSystemURL GetBaseFileSystemURLForVolume(VolumeInfo volume_info) {
    return PathToFileSystemURL(
        GetBasePathForVolume(temp_dir_.GetPath(), volume_info));
  }

  Profile* profile() { return profile_; }

 protected:
  content::BrowserTaskEnvironment task_environment_;
  TestingProfileManager profile_manager_;
  raw_ptr<TestingProfile, DanglingUntriaged> profile_;
  base::ScopedTempDir temp_dir_;
  const blink::StorageKey kTestStorageKey =
      blink::StorageKey::CreateFromStringForTesting("chrome://abc");
};

struct TestParam {
  TestParam(const char* name, const char* settings_value, size_t expected_id)
      : name(name), settings_value(settings_value), expected_id(expected_id) {}

  const char* name;
  const char* settings_value;
  size_t expected_id;
};

}  // namespace

using SourceDestinationMatcherAshTest = BaseTest;

TEST_F(SourceDestinationMatcherAshTest, NullptrSettingsNoCrash) {
  SourceDestinationMatcherAsh matcher;

  size_t id = 0;
  base::Value::List* settings = nullptr;
  matcher.AddFilters(&id, settings);
  EXPECT_EQ(id, 0u);
}

class SourceDestinationMatcherAshAddFilters
    : public BaseTest,
      public testing::WithParamInterface<TestParam> {};

TEST_P(SourceDestinationMatcherAshAddFilters, Test) {
  SourceDestinationMatcherAsh matcher;

  size_t id = 0;
  auto settings = base::JSONReader::Read(GetParam().settings_value,
                                         base::JSON_ALLOW_TRAILING_COMMAS);
  ASSERT_TRUE(settings.has_value());
  auto* settings_list = settings.value().GetIfList();
  ASSERT_TRUE(settings_list);
  matcher.AddFilters(&id, settings_list);
  EXPECT_EQ(id, GetParam().expected_id);
}

INSTANTIATE_TEST_SUITE_P(
    ,
    SourceDestinationMatcherAshAddFilters,
    testing::Values(
        // Validate that the enabled patterns match the expected patterns.
        TestParam("EmptySettings", R"([])", 0),
        TestParam("ListEntryIsNotADict", R"(["blub"])", 0),
        TestParam("ListEntryMissesSourceOrDestination",
                  R"([{
                      "sources": [""],
                    }])",
                  0),
        TestParam("ListEntrySourceOrDestinationNotADict",
                  R"([{
                      "sources": [{"file_system_type":"ANY"}],
                      "destinations": ["file_system_type"],
                    }])",
                  0),
        TestParam("SourceOrDestinationListEntriesEmpty",
                  R"([{
                      "sources": [{"file_system_type":"ANY"}],
                      "destinations": [{}],
                    }])",
                  0),
        TestParam("SourceOrDestinationListEntriesAreInvalid",
                  R"([{
                      "sources": [{"file_system_type":"ANY"}],
                      "destinations": [
                        {
                          "file_system_type":"ANY",
                          "unknown_key": "",
                        },
                      ],
                    }])",
                  0),
        TestParam("FileSystemTypeInvalid",
                  R"([{
                      "sources": [{"file_system_type":"ANY"}],
                      "destinations": [{"file_system_type":"BAD"}],
                    }])",
                  0),
        TestParam("Valid",
                  R"([{
                      "sources": [{"file_system_type":"ANY"}],
                      "destinations": [{"file_system_type":"ANY"}],
                    }])",
                  1),
        TestParam("ValidOneDestinationBad",
                  R"([{
                      "sources": [{"file_system_type":"ANY"}],
                      "destinations": [
                        {"file_system_type":"ANY"},
                        {"file_system_type":"BAD"},
                      ],
                    }])",
                  1)),
    [](const testing::TestParamInfo<TestParam>& info) {
      return info.param.name;
    });

class SourceDestinationMatcherAshParamTest
    : public BaseTest,
      public testing::WithParamInterface<VolumeInfo> {};

TEST_P(SourceDestinationMatcherAshParamTest, FromOneToAny) {
  VolumeInfo source_volume = GetParam();

  SourceDestinationMatcherAsh matcher;

  size_t id = 0;
  auto settings =
      base::JSONReader::Read(base::StringPrintf(R"([
          {
            "sources": [{"file_system_type":"%s"}],
            "destinations": [{"file_system_type":"ANY"}],
          }
        ])",
                                                source_volume.fs_config_string),
                             base::JSON_ALLOW_TRAILING_COMMAS);
  ASSERT_TRUE(settings.has_value());
  auto* settings_list = settings.value().GetIfList();
  ASSERT_TRUE(settings_list);
  matcher.AddFilters(&id, settings_list);
  // One rule should be added!
  EXPECT_EQ(id, 1u);

  for (auto src_test_info : kVolumeInfos) {
    bool should_match = std::string(src_test_info.fs_config_string) ==
                        std::string(source_volume.fs_config_string);
    for (auto dest_test_info : kVolumeInfos) {
      auto matches =
          matcher.Match(profile(), GetBaseFileSystemURLForVolume(src_test_info),
                        GetBaseFileSystemURLForVolume(dest_test_info));
      if (should_match) {
        ASSERT_EQ(matches.size(), 1u)
            << "matches: " << matches.size()
            << ", source: " << src_test_info.fs_config_string
            << ", destination: " << dest_test_info.fs_config_string;
        EXPECT_THAT(matches, testing::ElementsAre(1ul))
            << "source: " << src_test_info.fs_config_string
            << ", destination: " << dest_test_info.fs_config_string;
      } else {
        EXPECT_TRUE(matches.empty())
            << "matches: " << matches.size()
            << ", source: " << src_test_info.fs_config_string
            << ", destination: " << dest_test_info.fs_config_string;
      }
    }
  }
}

TEST_P(SourceDestinationMatcherAshParamTest, FromAnyToOne) {
  VolumeInfo destination_volume = GetParam();

  SourceDestinationMatcherAsh matcher;

  size_t id = 0;
  auto settings = base::JSONReader::Read(
      base::StringPrintf(R"([
          {
            "sources": [{"file_system_type":"ANY"}],
            "destinations": [{"file_system_type":"%s"}],
          }
        ])",
                         destination_volume.fs_config_string),
      base::JSON_ALLOW_TRAILING_COMMAS);
  ASSERT_TRUE(settings.has_value());
  auto* settings_list = settings.value().GetIfList();
  ASSERT_TRUE(settings_list);
  matcher.AddFilters(&id, settings_list);
  // One rule should be added!
  EXPECT_EQ(id, 1u);

  for (auto src_test_info : kVolumeInfos) {
    for (auto dest_test_info : kVolumeInfos) {
      bool should_match = std::string(dest_test_info.fs_config_string) ==
                          std::string(destination_volume.fs_config_string);
      auto matches =
          matcher.Match(profile(), GetBaseFileSystemURLForVolume(src_test_info),
                        GetBaseFileSystemURLForVolume(dest_test_info));
      if (should_match) {
        ASSERT_EQ(matches.size(), 1u)
            << "matches: " << matches.size()
            << ", source: " << src_test_info.fs_config_string
            << ", destination: " << dest_test_info.fs_config_string;
        EXPECT_THAT(matches, testing::ElementsAre(1ul))
            << "source: " << src_test_info.fs_config_string
            << ", destination: " << dest_test_info.fs_config_string;
      } else {
        EXPECT_TRUE(matches.empty())
            << "matches: " << matches.size()
            << ", source: " << src_test_info.fs_config_string
            << ", destination: " << dest_test_info.fs_config_string;
      }
    }
  }
}

INSTANTIATE_TEST_SUITE_P(,
                         SourceDestinationMatcherAshParamTest,
                         testing::ValuesIn(kVolumeInfos));

TEST(SourceDestinationMatcherAshFsTypeStringConversionTest, StringToType) {
  std::vector<std::pair<std::string, SourceDestinationMatcherAsh::FsType>>
      pairs{
          {"TESTING", SourceDestinationMatcherAsh::FsType::kTesting},
          {"UNKNOWN", SourceDestinationMatcherAsh::FsType::kUnknown},
          {"ANY", SourceDestinationMatcherAsh::FsType::kAny},
          {"*", SourceDestinationMatcherAsh::FsType::kAny},
          {"MY_FILES", SourceDestinationMatcherAsh::FsType::kMyFiles},
          {"REMOVABLE", SourceDestinationMatcherAsh::FsType::kRemovable},
          {"DEVICE_MEDIA_STORAGE",
           SourceDestinationMatcherAsh::FsType::kDeviceMediaStorage},
          {"PROVIDED", SourceDestinationMatcherAsh::FsType::kProvided},
          {"ARC", SourceDestinationMatcherAsh::FsType::kArc},
          {"GOOGLE_DRIVE", SourceDestinationMatcherAsh::FsType::kGoogleDrive},
          {"SMB", SourceDestinationMatcherAsh::FsType::kSmb},
          {"CROSTINI", SourceDestinationMatcherAsh::FsType::kCrostini},
          {"PLUGIN_VM", SourceDestinationMatcherAsh::FsType::kPluginVm},
          {"BOREALIS", SourceDestinationMatcherAsh::FsType::kBorealis},
          {"BRUSCHETTA", SourceDestinationMatcherAsh::FsType::kBruschetta},
          {"UNKNOWN_VM", SourceDestinationMatcherAsh::FsType::kUnknownVm},
      };
  for (auto [fs_string, expected_fs_type] : pairs) {
    EXPECT_EQ(expected_fs_type,
              SourceDestinationMatcherAsh::StringToFsType(fs_string));
  }
}

TEST(SourceDestinationMatcherAshFsTypeStringConversionTest, TypeToString) {
  std::vector<std::pair<SourceDestinationMatcherAsh::FsType, std::string>>
      pairs{
          {SourceDestinationMatcherAsh::FsType::kTesting, "TESTING"},
          {SourceDestinationMatcherAsh::FsType::kUnknown, "UNKNOWN"},
          {SourceDestinationMatcherAsh::FsType::kAny, "ANY"},
          {SourceDestinationMatcherAsh::FsType::kMyFiles, "MY_FILES"},
          {SourceDestinationMatcherAsh::FsType::kRemovable, "REMOVABLE"},
          {SourceDestinationMatcherAsh::FsType::kDeviceMediaStorage,
           "DEVICE_MEDIA_STORAGE"},
          {SourceDestinationMatcherAsh::FsType::kProvided, "PROVIDED"},
          {SourceDestinationMatcherAsh::FsType::kArc, "ARC"},
          {SourceDestinationMatcherAsh::FsType::kGoogleDrive, "GOOGLE_DRIVE"},
          {SourceDestinationMatcherAsh::FsType::kSmb, "SMB"},
          {SourceDestinationMatcherAsh::FsType::kCrostini, "CROSTINI"},
          {SourceDestinationMatcherAsh::FsType::kPluginVm, "PLUGIN_VM"},
          {SourceDestinationMatcherAsh::FsType::kBorealis, "BOREALIS"},
          {SourceDestinationMatcherAsh::FsType::kBruschetta, "BRUSCHETTA"},
          {SourceDestinationMatcherAsh::FsType::kUnknownVm, "UNKNOWN_VM"},
      };

  for (auto [fs_type, expected_fs_string] : pairs) {
    EXPECT_EQ(expected_fs_string,
              SourceDestinationMatcherAsh::FsTypeToString(fs_type));
  }
}

}  // namespace enterprise_connectors