chromium/chrome/browser/ash/extensions/file_manager/event_router_unittest.cc

// Copyright 2018 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/extensions/file_manager/event_router.h"

#include <memory>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/rand_util.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/values.h"
#include "chrome/browser/ash/extensions/file_manager/event_router_factory.h"
#include "chrome/browser/ash/file_manager/io_task.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/volume_manager_factory.h"
#include "chrome/browser/ash/fileapi/file_system_backend.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/scoped_testing_local_state.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/disks/fake_disk_mount_manager.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/test_event_router.h"
#include "extensions/common/extension.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/browser/test/test_file_system_context.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "ui/display/test/test_screen.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace file_manager {

TEST(EventRouterTest, PopulateCrostiniEvent) {
  extensions::api::file_manager_private::CrostiniEvent ext_event;
  url::Origin ext_origin = url::Origin::Create(
      extensions::Extension::GetBaseURLFromExtensionId("extensionid"));
  EventRouter::PopulateCrostiniEvent(
      ext_event,
      extensions::api::file_manager_private::CrostiniEventType::kUnshare,
      "vmname", ext_origin, "mountname", "filesystemname", "/full/path");

  EXPECT_EQ(ext_event.event_type,
            extensions::api::file_manager_private::CrostiniEventType::kUnshare);
  EXPECT_EQ(ext_event.vm_name, "vmname");
  EXPECT_EQ(ext_event.entries.size(), 1u);
  base::Value::Dict ext_props;
  ext_props.Set(
      "fileSystemRoot",
      "filesystem:chrome-extension://extensionid/external/mountname/");
  ext_props.Set("fileSystemName", "filesystemname");
  ext_props.Set("fileFullPath", "/full/path");
  ext_props.Set("fileIsDirectory", true);
  EXPECT_EQ(ext_event.entries[0].additional_properties, ext_props);

  extensions::api::file_manager_private::CrostiniEvent swa_event;
  url::Origin swa_origin = url::Origin::Create(
      GURL("chrome://file-manager/this-part-should-not-be-in?the=event"));
  EventRouter::PopulateCrostiniEvent(
      swa_event,
      extensions::api::file_manager_private::CrostiniEventType::kShare,
      "vmname", swa_origin, "mountname", "filesystemname", "/full/path");

  EXPECT_EQ(swa_event.event_type,
            extensions::api::file_manager_private::CrostiniEventType::kShare);
  EXPECT_EQ(swa_event.vm_name, "vmname");
  EXPECT_EQ(swa_event.entries.size(), 1u);
  base::Value::Dict swa_props;
  swa_props.Set("fileSystemRoot",
                "filesystem:chrome://file-manager/external/mountname/");
  swa_props.Set("fileSystemName", "filesystemname");
  swa_props.Set("fileFullPath", "/full/path");
  swa_props.Set("fileIsDirectory", true);
  EXPECT_EQ(swa_event.entries[0].additional_properties, swa_props);
}

namespace {

using ::base::test::RunClosure;
using ::testing::_;
using ::testing::AllOf;
using ::testing::Field;

// Observes the `BroadcastEvent` operation that is emitted by the event router.
// The mock methods are used to assert expectations on the return results.
class TestEventRouterObserver
    : public extensions::TestEventRouter::EventObserver {
 public:
  explicit TestEventRouterObserver(extensions::TestEventRouter* event_router)
      : event_router_(event_router) {
    event_router_->AddEventObserver(this);
  }
  ~TestEventRouterObserver() override {
    event_router_->RemoveEventObserver(this);
  }
  TestEventRouterObserver(const TestEventRouterObserver&) = delete;
  TestEventRouterObserver& operator=(const TestEventRouterObserver&) = delete;

  // TestEventRouter::EventObserver:
  MOCK_METHOD(void, OnBroadcastEvent, (const extensions::Event&), (override));
  MOCK_METHOD(void,
              OnDispatchEventToExtension,
              (const std::string&, const extensions::Event&),
              (override));

 private:
  raw_ptr<extensions::TestEventRouter> event_router_;
};

class FileManagerEventRouterTest : public testing::Test {
 public:
  FileManagerEventRouterTest()
      : scoped_testing_local_state_(TestingBrowserProcess::GetGlobal()) {}
  FileManagerEventRouterTest(const FileManagerEventRouterTest&) = delete;
  FileManagerEventRouterTest& operator=(const FileManagerEventRouterTest&) =
      delete;

  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());

    profile_ =
        std::make_unique<TestingProfile>(base::FilePath(temp_dir_.GetPath()));
    file_system_context_ = storage::CreateFileSystemContextForTesting(
        nullptr, temp_dir_.GetPath());

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

    storage::ExternalMountPoints::GetSystemInstance()->RegisterFileSystem(
        file_manager::util::GetDownloadsMountPointName(profile_.get()),
        storage::kFileSystemTypeLocal, storage::FileSystemMountOption(),
        temp_dir_.GetPath());

    auto* context = file_manager::util::GetFileSystemContextForSourceURL(
        profile_.get(), GURL("chrome-extension://abc"));
    ash::FileSystemBackend::Get(*context)->GrantFileAccessToOrigin(
        url::Origin::Create(GURL("chrome-extension://abc")),
        base::FilePath(
            file_manager::util::GetDownloadsMountPointName(profile_.get())));
  }

  const io_task::EntryStatus CreateSuccessfulEntryStatusForFileName(
      const std::string& file_name) {
    const base::FilePath file_path = temp_dir_.GetPath().Append(file_name);
    EXPECT_TRUE(base::WriteFile(file_path, base::RandBytesAsString(32)));

    storage::FileSystemURL url =
        file_system_context_->CreateCrackedFileSystemURL(
            kTestStorageKey, storage::kFileSystemTypeTest, file_path);

    return io_task::EntryStatus(std::move(url), base::File::FILE_OK);
  }

  ScopedTestingLocalState scoped_testing_local_state_;
  content::BrowserTaskEnvironment task_environment_;
  display::test::TestScreen test_screen_{/*create_display=*/true,
                                         /*register_screen=*/true};
  base::ScopedTempDir temp_dir_;
  ash::disks::FakeDiskMountManager disk_mount_manager_;
  std::unique_ptr<TestingProfile> profile_;
  const blink::StorageKey kTestStorageKey =
      blink::StorageKey::CreateFromStringForTesting("chrome-extension://abc");
  scoped_refptr<storage::FileSystemContext> file_system_context_;
};

MATCHER(ExpectNoArgs, "") {
  return arg.size() == 0;
}

// A matcher that matches an `extensions::Event::event_args` and attempts to
// extract the "outputs" key. It then looks at the output at `index` and matches
// the `field` against the `expected_value`.
MATCHER_P3(ExpectEventArgString, index, field, expected_value, "") {
  EXPECT_GE(arg.size(), 1u);
  const base::Value::List* outputs = arg[0].GetDict().FindList("outputs");
  EXPECT_TRUE(outputs) << "The outputs field is not available on the event";
  EXPECT_GT(outputs->size(), index)
      << "The supplied index on outputs is not available, size: "
      << outputs->size() << ", index: " << index;
  const std::string* actual_value =
      (*outputs)[index].GetDict().FindString(field);
  EXPECT_TRUE(actual_value) << "Could not find the string with key: " << field;
  return testing::ExplainMatchResult(expected_value, *actual_value,
                                     result_listener);
}

// A matcher that matches an `extensions::Event::event_args` and attempts to
// extract the "conflictParams" and "pauseParams" keys. It expects
// "conflictParams" to be empty, and then matches the "policyParams" values
// against the expected ones.
MATCHER_P4(ExpectEventArgPauseParams,
           expected_type,
           expected_count,
           expected_file_name,
           expected_always_show_review,
           "") {
  EXPECT_GE(arg.size(), 1u);
  const base::Value::Dict* pause_params =
      arg[0].GetDict().FindDict("pauseParams");
  EXPECT_TRUE(pause_params)
      << "The pause_params field is not available on the event";

  const base::Value::Dict* conflict_pause_params =
      pause_params->FindDict("conflictParams");
  EXPECT_FALSE(conflict_pause_params)
      << "The conflictParams field should not be available on the event";

  const base::Value::Dict* policy_pause_params =
      pause_params->FindDict("policyParams");
  EXPECT_TRUE(policy_pause_params)
      << "The policyParams field is not available on the event";
  const std::string* actual_type = policy_pause_params->FindString("type");
  EXPECT_TRUE(actual_type) << "Could not find the string with key: type";
  const std::optional<int> actual_count =
      policy_pause_params->FindInt("policyFileCount");
  EXPECT_TRUE(actual_count.has_value())
      << "Could not find the number with key: type";
  const std::string* actual_file_name =
      policy_pause_params->FindString("fileName");
  EXPECT_TRUE(actual_file_name)
      << "Could not find the string with key: fileName";
  const std::optional<bool> actual_always_show_review =
      policy_pause_params->FindBool("alwaysShowReview");
  EXPECT_TRUE(actual_always_show_review.has_value())
      << "Could not find the string with key: alwaysShowReview";
  return testing::ExplainMatchResult(expected_type, *actual_type,
                                     result_listener) &&
         testing::ExplainMatchResult(expected_count, actual_count.value(),
                                     result_listener) &&
         testing::ExplainMatchResult(expected_file_name, *actual_file_name,
                                     result_listener) &&
         testing::ExplainMatchResult(expected_always_show_review,
                                     actual_always_show_review.value(),
                                     result_listener);
}

// A matcher that matches an `extensions::Event::event_args` and attempts to
// extract the "policyError" key. It then matches the "policyError" values
// against the expected ones.
MATCHER_P4(ExpectEventArgPolicyError,
           expected_type,
           expected_count,
           expected_file_name,
           expected_always_show_review,
           "") {
  EXPECT_GE(arg.size(), 1u);
  const base::Value::Dict* policy_error =
      arg[0].GetDict().FindDict("policyError");
  EXPECT_TRUE(policy_error)
      << "The policyError field is not available on the event";

  const std::string* actual_type = policy_error->FindString("type");
  EXPECT_TRUE(actual_type) << "Could not find the string with key: type";
  const std::optional<int> actual_count =
      policy_error->FindInt("policyFileCount");
  EXPECT_TRUE(actual_count.has_value())
      << "Could not find the string with key: type";
  const std::string* actual_file_name = policy_error->FindString("fileName");
  EXPECT_TRUE(actual_file_name)
      << "Could not find the string with key: fileName";
  const std::optional<bool> actual_always_show_review =
      policy_error->FindBool("alwaysShowReview");
  EXPECT_TRUE(actual_always_show_review.has_value())
      << "Could not find the string with key: alwaysShowReview";
  return testing::ExplainMatchResult(expected_type, *actual_type,
                                     result_listener) &&
         testing::ExplainMatchResult(expected_count, actual_count.value(),
                                     result_listener) &&
         testing::ExplainMatchResult(expected_file_name, *actual_file_name,
                                     result_listener) &&
         testing::ExplainMatchResult(expected_always_show_review,
                                     actual_always_show_review.value(),
                                     result_listener);
}

TEST_F(FileManagerEventRouterTest, OnIOTaskStatusForTrash) {
  // Setup event routers.
  extensions::TestEventRouter* test_event_router =
      extensions::CreateAndUseTestEventRouter(profile_.get());
  TestEventRouterObserver observer(test_event_router);
  auto event_router = std::make_unique<EventRouter>(profile_.get());
  event_router->ForceBroadcastingForTesting(true);

  io_task::EntryStatus source_entry =
      CreateSuccessfulEntryStatusForFileName("foo.txt");
  io_task::EntryStatus output_entry =
      CreateSuccessfulEntryStatusForFileName("bar.txt");

  std::vector<io_task::EntryStatus> source_entries;
  source_entries.push_back(std::move(source_entry));
  std::vector<io_task::EntryStatus> output_entries;
  output_entries.push_back(std::move(output_entry));

  // Setup the ProgressStatus event that expects
  file_manager::io_task::ProgressStatus status;
  status.type = file_manager::io_task::OperationType::kTrash;
  status.state = file_manager::io_task::State::kSuccess;
  status.sources = std::move(source_entries);
  status.outputs = std::move(output_entries);

  base::RunLoop run_loop;
  EXPECT_CALL(
      observer,
      OnBroadcastEvent(Field(
          &extensions::Event::event_args,
          AllOf(ExpectEventArgString(0u, "fileFullPath", "/bar.txt"),
                ExpectEventArgString(0u, "fileSystemName", "Downloads"),
                ExpectEventArgString(
                    0u, "fileSystemRoot",
                    "filesystem:chrome-extension://abc/external/Downloads/")))))
      .WillOnce(RunClosure(run_loop.QuitClosure()));

  event_router->OnIOTaskStatus(status);
  run_loop.Run();
}

TEST_F(FileManagerEventRouterTest, OnIOTaskStatusForCopyPause) {
  // Setup event routers.
  extensions::TestEventRouter* test_event_router =
      extensions::CreateAndUseTestEventRouter(profile_.get());
  TestEventRouterObserver observer(test_event_router);
  auto event_router = std::make_unique<EventRouter>(profile_.get());
  event_router->ForceBroadcastingForTesting(true);

  io_task::EntryStatus source_entry =
      CreateSuccessfulEntryStatusForFileName("foo.txt");

  std::vector<io_task::EntryStatus> source_entries;
  source_entries.push_back(std::move(source_entry));

  // Setup the ProgressStatus event.
  file_manager::io_task::ProgressStatus status;
  status.type = file_manager::io_task::OperationType::kCopy;
  status.state = file_manager::io_task::State::kPaused;
  status.sources = std::move(source_entries);
  status.pause_params.policy_params = io_task::PolicyPauseParams(
      policy::Policy::kDlp, /*warning_files_count*/ 2u, "foo.txt",
      /*always_show_review=*/false);

  // Expect the event to have dlp as policy pause params.
  base::RunLoop run_loop;
  EXPECT_CALL(observer,
              OnBroadcastEvent(Field(&extensions::Event::event_args,
                                     AllOf(ExpectEventArgPauseParams(
                                         "dlp", 2, "foo.txt", false)))))
      .WillOnce(RunClosure(run_loop.QuitClosure()));

  event_router->OnIOTaskStatus(status);
  run_loop.Run();
}

TEST_F(FileManagerEventRouterTest, OnIOTaskStatusForPolicyError) {
  // Setup event routers.
  extensions::TestEventRouter* test_event_router =
      extensions::CreateAndUseTestEventRouter(profile_.get());
  TestEventRouterObserver observer(test_event_router);
  auto event_router = std::make_unique<EventRouter>(profile_.get());
  event_router->ForceBroadcastingForTesting(true);

  io_task::EntryStatus source_entry =
      CreateSuccessfulEntryStatusForFileName("foo.txt");

  std::vector<io_task::EntryStatus> source_entries;
  source_entries.push_back(std::move(source_entry));

  // Setup the ProgressStatus event.
  file_manager::io_task::ProgressStatus status;
  status.type = file_manager::io_task::OperationType::kCopy;
  status.state = file_manager::io_task::State::kError;
  status.sources = std::move(source_entries);
  status.policy_error.emplace(io_task::PolicyErrorType::kDlp,
                              /*blocked_files=*/1, "foo.txt",
                              /*always_show_review=*/true);

  // Expect the event to have dlp as policy error.
  base::RunLoop run_loop;
  EXPECT_CALL(observer,
              OnBroadcastEvent(Field(
                  &extensions::Event::event_args,
                  AllOf(ExpectEventArgPolicyError("dlp", 1, "foo.txt", true)))))
      .WillOnce(RunClosure(run_loop.QuitClosure()));

  event_router->OnIOTaskStatus(status);
  run_loop.Run();
}

class FileManagerEventRouterLocalFilesTest : public FileManagerEventRouterTest {
 public:
  FileManagerEventRouterLocalFilesTest() {
    scoped_feature_list_.InitAndEnableFeature(features::kSkyVault);
  }
  ~FileManagerEventRouterLocalFilesTest() override = default;

  void SetLocalUserFilesPolicy(bool allowed) {
    scoped_testing_local_state_.Get()->SetBoolean(prefs::kLocalUserFilesAllowed,
                                                  allowed);
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

TEST_F(FileManagerEventRouterLocalFilesTest, OnLocalUserFilesPolicyChanged) {
  // Set up event routers.
  extensions::TestEventRouter* test_event_router =
      extensions::CreateAndUseTestEventRouter(profile_.get());
  TestEventRouterObserver observer(test_event_router);
  auto event_router = std::make_unique<EventRouter>(profile_.get());
  event_router->ForceBroadcastingForTesting(true);

  // Expect the preferences changed event.
  base::Value::List event_args =
      extensions::api::file_manager_private::OnPreferencesChanged::Create();
  base::RunLoop run_loop;
  EXPECT_CALL(observer, OnBroadcastEvent(Field(&extensions::Event::event_args,
                                               AllOf(ExpectNoArgs()))))
      .WillOnce(RunClosure(run_loop.QuitClosure()));
  SetLocalUserFilesPolicy(/*allowed=*/false);
  run_loop.Run();
}

}  // namespace
}  // namespace file_manager