chromium/chrome/browser/ash/file_manager/file_tasks_unittest.cc

// Copyright 2013 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/file_manager/file_tasks.h"

#include <algorithm>
#include <memory>
#include <set>
#include <string_view>
#include <utility>

#include "ash/constants/ash_features.h"
#include "base/command_line.h"
#include "base/containers/to_vector.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/ranges/algorithm.h"
#include "base/run_loop.h"
#include "base/strings/escape.h"
#include "base/strings/string_split.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#include "chrome/browser/apps/app_service/app_service_proxy_ash.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/app_service_test.h"
#include "chrome/browser/apps/app_service/policy_util.h"
#include "chrome/browser/ash/crostini/crostini_pref_names.h"
#include "chrome/browser/ash/crostini/crostini_test_helper.h"
#include "chrome/browser/ash/crostini/fake_crostini_features.h"
#include "chrome/browser/ash/drive/file_system_util.h"
#include "chrome/browser/ash/file_manager/app_id.h"
#include "chrome/browser/ash/file_manager/app_service_file_tasks.h"
#include "chrome/browser/ash/file_manager/file_manager_test_util.h"
#include "chrome/browser/ash/file_manager/office_file_tasks.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/virtual_file_tasks.h"
#include "chrome/browser/ash/file_manager/virtual_tasks/fake_virtual_task.h"
#include "chrome/browser/ash/file_manager/virtual_tasks/id_constants.h"
#include "chrome/browser/ash/guest_os/guest_os_mime_types_service.h"
#include "chrome/browser/ash/guest_os/guest_os_mime_types_service_factory.h"
#include "chrome/browser/ash/settings/scoped_cros_settings_test_helper.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_rules_manager_factory.h"
#include "chrome/browser/chromeos/policy/dlp/test/mock_dlp_rules_manager.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/dbus/concierge/concierge_client.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/testing_pref_service.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/features.h"
#include "components/services/app_service/public/cpp/file_handler.h"
#include "components/services/app_service/public/cpp/file_handler_info.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/browser/entry_info.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/manifest.h"
#include "google_apis/drive/drive_api_parser.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace file_manager::file_tasks {

TEST(FileManagerFileTasksTest, FullTaskDescriptor_WithIconAndDefault) {
  FullTaskDescriptor full_descriptor(
      TaskDescriptor("app-id", TASK_TYPE_FILE_BROWSER_HANDLER, "action-id"),
      "task title", GURL("http://example.com/icon.png"), true /* is_default */,
      false /* is_generic_file_handler */, false /* is_file_extension_match */);

  EXPECT_EQ("app-id", full_descriptor.task_descriptor.app_id);
  EXPECT_EQ(TaskType::TASK_TYPE_FILE_BROWSER_HANDLER,
            full_descriptor.task_descriptor.task_type);
  EXPECT_EQ("action-id", full_descriptor.task_descriptor.action_id);
  EXPECT_EQ("http://example.com/icon.png", full_descriptor.icon_url.spec());
  EXPECT_EQ("task title", full_descriptor.task_title);
  EXPECT_TRUE(full_descriptor.is_default);
}

TEST(FileManagerFileTasksTest, MakeTaskID) {
  EXPECT_EQ("app-id|file|action-id",
            MakeTaskID("app-id", TASK_TYPE_FILE_BROWSER_HANDLER, "action-id"));
  EXPECT_EQ("app-id|app|action-id",
            MakeTaskID("app-id", TASK_TYPE_FILE_HANDLER, "action-id"));
}

TEST(FileManagerFileTasksTest, TaskDescriptorToId) {
  EXPECT_EQ("app-id|file|action-id",
            TaskDescriptorToId(TaskDescriptor(
                "app-id", TASK_TYPE_FILE_BROWSER_HANDLER, "action-id")));
}

TEST(FileManagerFileTasksTest, ParseTaskID_FileBrowserHandler) {
  EXPECT_EQ(
      ParseTaskID("app-id|file|action-id"),
      TaskDescriptor("app-id", TASK_TYPE_FILE_BROWSER_HANDLER, "action-id"));
}

TEST(FileManagerFileTasksTest, ParseTaskID_FileHandler) {
  EXPECT_EQ(ParseTaskID("app-id|app|action-id"),
            TaskDescriptor("app-id", TASK_TYPE_FILE_HANDLER, "action-id"));
}

TEST(FileManagerFileTasksTest, ParseTaskID_Legacy) {
  // A legacy task ID only has two parts. The task type should be
  // TASK_TYPE_FILE_BROWSER_HANDLER.
  EXPECT_EQ(
      ParseTaskID("app-id|action-id"),
      TaskDescriptor("app-id", TASK_TYPE_FILE_BROWSER_HANDLER, "action-id"));
}

TEST(FileManagerFileTasksTest, ParseTaskID_Invalid) {
  EXPECT_FALSE(ParseTaskID("invalid"));
}

TEST(FileManagerFileTasksTest, ParseTaskID_UnknownTaskType) {
  EXPECT_FALSE(ParseTaskID("app-id|unknown|action-id"));
}

TEST(FileManagerFileTasksTest, BaseContainsFindsTaskDescriptors) {
  // Create several task descriptors that each have one field with the
  // "smallest" value in that category. i.e. task_1 has the "smallest" app_id
  // value, task_2 has the "smallest" task type value, and task_3 has the
  // "smallest" action_id value.
  TaskDescriptor task_1("a", TASK_TYPE_ARC_APP, "other");
  TaskDescriptor task_2("b", TASK_TYPE_FILE_BROWSER_HANDLER, "view");
  TaskDescriptor task_3("c", TASK_TYPE_FILE_HANDLER, "edit");

  std::set<TaskDescriptor> tasks;
  tasks.insert(task_1);
  tasks.insert(task_2);
  tasks.insert(task_3);

  ASSERT_TRUE(base::Contains(tasks, task_1));
  ASSERT_TRUE(base::Contains(tasks, task_2));
  ASSERT_TRUE(base::Contains(tasks, task_3));
}

TEST(FileManagerFileTasksTest, EqualTaskDescriptors) {
  TaskDescriptor task_1("a", TASK_TYPE_FILE_HANDLER, "view");
  TaskDescriptor task_2("a", TASK_TYPE_FILE_HANDLER, "view");

  ASSERT_EQ(task_1, task_2);
}

TEST(FileManagerFileTasksTest, NotEqualAppIdInTaskDescriptors) {
  TaskDescriptor task_1("a", TASK_TYPE_FILE_HANDLER, "view");
  TaskDescriptor task_2("b", TASK_TYPE_FILE_HANDLER, "view");

  ASSERT_NE(task_1, task_2);
}

TEST(FileManagerFileTasksTest, NotEqualTaskTypeInTaskDescriptors) {
  TaskDescriptor task_1("a", TASK_TYPE_FILE_HANDLER, "view");
  TaskDescriptor task_2("a", TASK_TYPE_FILE_BROWSER_HANDLER, "view");

  ASSERT_NE(task_1, task_2);
}

TEST(FileManagerFileTasksTest, NotEqualActionIdInTaskDescriptors) {
  TaskDescriptor task_1("a", TASK_TYPE_FILE_HANDLER, "view");
  TaskDescriptor task_2("a", TASK_TYPE_FILE_HANDLER, "edit");

  ASSERT_NE(task_1, task_2);
}

// Test FileHandlerIsEnabled which returns whether a file handler should be
// used.
TEST(FileManagerFileTasksTest, FileHandlerIsEnabled) {
  content::BrowserTaskEnvironment task_environment;
  TestingProfile test_profile;
  crostini::FakeCrostiniFeatures crostini_features;

  const std::string test_id = "test";

  crostini_features.set_export_import_ui_allowed(true);
  EXPECT_TRUE(
      FileHandlerIsEnabled(&test_profile, kFileManagerSwaAppId,
                           "chrome://file-manager/?import-crostini-image"));
  EXPECT_TRUE(
      FileHandlerIsEnabled(&test_profile, kFileManagerSwaAppId, test_id));

  crostini_features.set_export_import_ui_allowed(false);
  EXPECT_FALSE(
      FileHandlerIsEnabled(&test_profile, kFileManagerSwaAppId,
                           "chrome://file-manager/?import-crostini-image"));
  EXPECT_TRUE(
      FileHandlerIsEnabled(&test_profile, kFileManagerSwaAppId, test_id));

  crostini_features.set_root_access_allowed(true);
  EXPECT_TRUE(
      FileHandlerIsEnabled(&test_profile, kFileManagerSwaAppId,
                           "chrome://file-manager/?install-linux-package"));
  EXPECT_TRUE(
      FileHandlerIsEnabled(&test_profile, kFileManagerSwaAppId, test_id));

  crostini_features.set_root_access_allowed(false);
  EXPECT_FALSE(
      FileHandlerIsEnabled(&test_profile, kFileManagerSwaAppId,
                           "chrome://file-manager/?install-linux-package"));
  EXPECT_TRUE(
      FileHandlerIsEnabled(&test_profile, kFileManagerSwaAppId, test_id));
}

class FileManagerFileTaskWithAppServiceTest : public testing::Test {
 public:
  void SetUp() override {
    TestingProfile::Builder profile_builder;
    profile_ = profile_builder.Build();
    app_service_test_.SetUp(profile_.get());
    app_service_proxy_ =
        apps::AppServiceProxyFactory::GetForProfile(profile_.get());
    ASSERT_TRUE(app_service_proxy_);
  }

  void AddFakeAppToAppService(const std::string& app_id,
                              const std::optional<std::string>& package_name,
                              std::vector<std::string> policy_ids,
                              apps::AppType app_type) {
    auto app = std::make_unique<apps::App>(app_type, app_id);
    app->app_id = app_id;
    app->app_type = app_type;
    app->publisher_id = package_name;
    app->policy_ids = std::move(policy_ids);
    app->readiness = apps::Readiness::kReady;

    std::vector<apps::AppPtr> apps;
    apps.push_back(std::move(app));
    app_service_proxy()->OnApps(std::move(apps), app_type,
                                false /* should_notify_initialized */);
  }

  TestingProfile* profile() { return profile_.get(); }
  apps::AppServiceProxy* app_service_proxy() { return app_service_proxy_; }

 private:
  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<TestingProfile> profile_;
  raw_ptr<apps::AppServiceProxy> app_service_proxy_ = nullptr;
  apps::AppServiceTest app_service_test_;
};

using AppIdPolicyIdPair = std::pair<const char*, const char*>;

class FileManagerFileTaskPolicyDefaultHandlersTest
    : public FileManagerFileTaskWithAppServiceTest {
 public:
  void SetUp() override {
    FileManagerFileTaskWithAppServiceTest::SetUp();
    CreateAppsAndTasks();
  }

  void TearDown() override { GetTestVirtualTasks().clear(); }

 protected:
  void UpdateDefaultHandlersPrefs(
      const std::vector<std::pair<std::string, std::string>>& handlers = {}) {
    base::Value::Dict pref_dict;
    for (const auto& [file_extension, policy_id] : handlers) {
      pref_dict.Set(file_extension, policy_id);
    }
    profile()->GetTestingPrefService()->SetDict(
        prefs::kDefaultHandlersForFileExtensions, std::move(pref_dict));
  }

  ResultingTasks* resulting_tasks() { return resulting_tasks_.get(); }
  std::vector<extensions::EntryInfo>& entries() { return entries_; }

  void CheckCorrectPolicyAssignment(std::string_view default_app_id) {
    ASSERT_EQ(resulting_tasks()->policy_default_handler_status,
              PolicyDefaultHandlerStatus::kDefaultHandlerAssignedByPolicy);
    ASSERT_EQ(base::ranges::count_if(resulting_tasks()->tasks, &IsDefaultTask),
              1);
    ASSERT_EQ(base::ranges::find_if(resulting_tasks()->tasks, &IsDefaultTask)
                  ->task_descriptor.app_id,
              default_app_id);
  }

  void CheckCorrectPolicyAssignmentForVirtualTask(
      std::string_view virtual_task_id) {
    ASSERT_EQ(resulting_tasks()->policy_default_handler_status,
              PolicyDefaultHandlerStatus::kDefaultHandlerAssignedByPolicy);
    ASSERT_EQ(base::ranges::count_if(resulting_tasks()->tasks, &IsDefaultTask),
              1);
    const auto& task =
        base::ranges::find_if(resulting_tasks()->tasks, &IsDefaultTask)
            ->task_descriptor;
    ASSERT_TRUE(IsVirtualTask(task));
    ASSERT_THAT(task.action_id, testing::EndsWith(virtual_task_id));
  }

  void CheckConflictingPolicyAssignment() {
    ASSERT_EQ(resulting_tasks()->policy_default_handler_status,
              PolicyDefaultHandlerStatus::kIncorrectAssignment);
    ASSERT_EQ(base::ranges::count_if(resulting_tasks()->tasks, &IsDefaultTask),
              0);
  }

  void CheckNoPolicyAssignment() {
    ASSERT_FALSE(resulting_tasks()->policy_default_handler_status);
    ASSERT_EQ(base::ranges::count_if(resulting_tasks()->tasks, &IsDefaultTask),
              0);
  }

 protected:
  static constexpr char kWebAppId[] = "web-app-id";
  static constexpr char kChromeAppId[] = "chrome-app-id";
  static constexpr char kArcAppId[] = "arc-app-id";
  static constexpr char kNonExistentAppId[] = "null";
  static constexpr char kIsolatedAppId[] = "ghgjflengkicinnmfeejkpjmcohegmid";
  static constexpr char kIsolatedPolicyId[] =
      "w2gqjem6b4m7vhiqpjr3btcpp7dxfyjt6h4uuyuxklcsmygtgncaaaac";

  static constexpr char kWebAppUrl[] = "https://web.app";
  static constexpr char kArcAppPackageName[] = "com.package.name";

  static constexpr AppIdPolicyIdPair kAppIdPolicyIdMapping[] = {
      {kWebAppId, kWebAppUrl},
      {kArcAppId, kArcAppPackageName},
      {kChromeAppId, kChromeAppId},
      {kIsolatedAppId, kIsolatedPolicyId}};

 private:
  void CreateAppsAndTasks() {
    resulting_tasks_ = std::make_unique<ResultingTasks>();

    std::vector<FullTaskDescriptor>& tasks = resulting_tasks()->tasks;
    for (const auto& [app_id, _] : kAppIdPolicyIdMapping) {
      tasks.emplace_back(
          TaskDescriptor{app_id, TASK_TYPE_FILE_HANDLER, "action-id"},
          /*task_title=*/"Task", GURL("https://example.com/app.png"), false,
          false, false);
    }

    AddFakeAppToAppService(kWebAppId, /*package_name=*/{},
                           /*policy_ids=*/{kWebAppUrl}, apps::AppType::kWeb);
    AddFakeAppToAppService(kChromeAppId, /*package_name=*/{},
                           /*policy_ids=*/{kChromeAppId},
                           apps::AppType::kChromeApp);
    AddFakeAppToAppService(kArcAppId, /*package_name=*/kArcAppPackageName,
                           /*policy_ids=*/{kArcAppPackageName},
                           apps::AppType::kArc);
    AddFakeAppToAppService(kIsolatedAppId, /*package_name=*/{},
                           /*policy_ids=*/{kIsolatedPolicyId},
                           apps::AppType::kWeb);
  }

  static bool IsDefaultTask(const FullTaskDescriptor& ftd) {
    return ftd.is_default;
  }

  std::unique_ptr<ResultingTasks> resulting_tasks_;
  std::vector<extensions::EntryInfo> entries_;
};

// Check that no default tasks are set if no policy is set.
TEST_F(FileManagerFileTaskPolicyDefaultHandlersTest, CheckNoPolicyAssignment) {
  entries().emplace_back(base::FilePath::FromUTF8Unsafe("foo.txt"),
                         "text/plain", false);

  UpdateDefaultHandlersPrefs(/*empty*/);
  ASSERT_FALSE(ChooseAndSetDefaultTaskFromPolicyPrefs(profile(), entries(),
                                                      resulting_tasks()));
  CheckNoPolicyAssignment();
}

// Check that a policy set to a non-existent app is ignored.
TEST_F(FileManagerFileTaskPolicyDefaultHandlersTest,
       CheckAssignmentToNonExistentApp) {
  entries().emplace_back(base::FilePath::FromUTF8Unsafe("foo.txt"),
                         "text/plain", false);

  UpdateDefaultHandlersPrefs({{".txt", kNonExistentAppId}});
  ASSERT_FALSE(ChooseAndSetDefaultTaskFromPolicyPrefs(profile(), entries(),
                                                      resulting_tasks()));
  CheckNoPolicyAssignment();
}

// Check that assigning different apps to handle different file extensions
// leads to a conflict.
TEST_F(FileManagerFileTaskPolicyDefaultHandlersTest,
       CheckConflictingPolicyAssignment) {
  entries().emplace_back(base::FilePath::FromUTF8Unsafe("foo.txt"),
                         "text/plain", false);
  entries().emplace_back(base::FilePath::FromUTF8Unsafe("foo.csv"), "text/csv",
                         false);

  UpdateDefaultHandlersPrefs({{".txt", kWebAppUrl}, {".csv", kChromeAppId}});
  ASSERT_TRUE(ChooseAndSetDefaultTaskFromPolicyPrefs(profile(), entries(),
                                                     resulting_tasks()));
  CheckConflictingPolicyAssignment();
}

class FileManagerFileTaskVirtualTaskPolicyDefaultHandlersTest
    : public FileManagerFileTaskPolicyDefaultHandlersTest,
      public testing::WithParamInterface<
          std::tuple<std::string, std::string, std::string>> {
 public:
  FileManagerFileTaskVirtualTaskPolicyDefaultHandlersTest() {
    // These feature flags are required to make different virtual tasks
    // discoverable.
    features_.InitWithFeatures(
        {features::kIsolatedWebApps, features::kIsolatedWebAppUnmanagedInstall,
         chromeos::features::kUploadOfficeToCloud},
        {});
  }

 private:
  base::test::ScopedFeatureList features_;
};

// Check that virtual tasks are handled by the policy.
TEST_P(FileManagerFileTaskVirtualTaskPolicyDefaultHandlersTest, VirtualTask) {
  auto [policy_id, action_id, file_extension] = GetParam();

  const std::string file_name = base::StrCat({"foo", file_extension});
  entries().emplace_back(base::FilePath::FromUTF8Unsafe(file_name),
                         /*mime_type=*/"", /*is_directory=*/false);

  ASSERT_EQ(entries().size(), 1U);
  MatchVirtualTasks(
      profile(), entries(),
      /*file_urls=*/
      {GURL(base::StrCat(
          {"filesystem:chrome://file-manager/external/", file_name}))},
      /*dlp_source_urls=*/{}, &resulting_tasks()->tasks);

  UpdateDefaultHandlersPrefs(
      {{file_extension,
        base::StrCat({apps_util::kVirtualTaskPrefix, policy_id})}});
  ASSERT_TRUE(ChooseAndSetDefaultTaskFromPolicyPrefs(profile(), entries(),
                                                     resulting_tasks()));
  CheckCorrectPolicyAssignmentForVirtualTask(action_id);
}

INSTANTIATE_TEST_SUITE_P(
    /**/,
    FileManagerFileTaskVirtualTaskPolicyDefaultHandlersTest,
    testing::Values(
        std::make_tuple("install-isolated-web-app",
                        kActionIdInstallIsolatedWebApp,
                        ".swbn"),
        std::make_tuple("microsoft-office", kActionIdOpenInOffice, ".docx"),
        std::make_tuple("google-docs", kActionIdWebDriveOfficeWord, ".docx"),
        std::make_tuple("google-spreadsheets",
                        kActionIdWebDriveOfficeExcel,
                        ".xlsx"),
        std::make_tuple("google-slides",
                        kActionIdWebDriveOfficePowerPoint,
                        ".pptx")),
    [](const auto& info) {
      const auto& policy_id = std::get<0>(info.param);
      // GoogleTest doesn't allow dashes in test names; the code below
      // changes `xxx-yyy-zzz` policy ids to `XxxYyyZzz` test names.
      return base::JoinString(
          base::ToVector(
              base::SplitString(policy_id, "-",
                                base::WhitespaceHandling::TRIM_WHITESPACE,
                                base::SplitResult::SPLIT_WANT_NONEMPTY),
              [](const std::string& piece) {
                return base::ToUpperASCII(piece[0]) + piece.substr(1);
              }),
          "");
    });

// Check that incorrectly assigned virtual tasks are ignored.
TEST_F(FileManagerFileTaskPolicyDefaultHandlersTest,
       VirtualTaskIncorrectAssignment) {
  auto virtual_task = std::make_unique<FakeVirtualTask>(
      ToSwaActionId(kActionIdInstallIsolatedWebApp));
  GetTestVirtualTasks().push_back(virtual_task.get());

  constexpr char kFileName[] = "foo.txt";

  MatchVirtualTasks(
      profile(),
      {{base::FilePath::FromUTF8Unsafe(kFileName), "text/plain",
        /*is_directory=*/false}},
      /*file_urls=*/
      {GURL(base::StrCat(
          {"filesystem:chrome://file-manager/external/", kFileName}))},
      /*dlp_source_urls=*/{}, &resulting_tasks()->tasks);

  constexpr char kNonExistentVirtualTaskActionId[] = "incorrect-virtual-id";

  UpdateDefaultHandlersPrefs(
      {{".txt", base::StrCat({apps_util::kVirtualTaskPrefix,
                              kNonExistentVirtualTaskActionId})}});
  entries().emplace_back(base::FilePath::FromUTF8Unsafe(kFileName),
                         "text/plain", false);
  ASSERT_FALSE(ChooseAndSetDefaultTaskFromPolicyPrefs(profile(), entries(),
                                                      resulting_tasks()));
  CheckNoPolicyAssignment();
}

class FileManagerFileTaskPolicyDefaultHandlersTestPerAppType
    : public FileManagerFileTaskPolicyDefaultHandlersTest,
      public testing::WithParamInterface<AppIdPolicyIdPair> {
 public:
  // This is required to correctly instantiate TEST_SUITE_P.
  using FileManagerFileTaskPolicyDefaultHandlersTest::kAppIdPolicyIdMapping;
};

// Check that default tasks are set correctly by policy_id.
TEST_P(FileManagerFileTaskPolicyDefaultHandlersTestPerAppType,
       ChooseAndSetDefaultTaskFromPolicyPrefsForSingleFileExtension) {
  entries().emplace_back(base::FilePath::FromUTF8Unsafe("foo.txt"),
                         "text/plain", false);

  const auto [app_id, policy_id] = GetParam();
  UpdateDefaultHandlersPrefs({{".txt", policy_id}});
  ASSERT_TRUE(ChooseAndSetDefaultTaskFromPolicyPrefs(profile(), entries(),
                                                     resulting_tasks()));
  CheckCorrectPolicyAssignment(app_id);
}

// Check that default tasks are set correctly by policy_id for multiple
// file_extensions.
TEST_P(FileManagerFileTaskPolicyDefaultHandlersTestPerAppType,
       ChooseAndSetDefaultTaskFromPolicyPrefsForMultipleFileExtensions) {
  entries().emplace_back(base::FilePath::FromUTF8Unsafe("foo.txt"),
                         "text/plain", false);
  entries().emplace_back(base::FilePath::FromUTF8Unsafe("foo.csv"), "text/csv",
                         false);

  const auto [app_id, policy_id] = GetParam();
  UpdateDefaultHandlersPrefs({{".txt", policy_id}, {".csv", policy_id}});
  ASSERT_TRUE(ChooseAndSetDefaultTaskFromPolicyPrefs(profile(), entries(),
                                                     resulting_tasks()));
  CheckCorrectPolicyAssignment(app_id);
}

INSTANTIATE_TEST_SUITE_P(
    /**/,
    FileManagerFileTaskPolicyDefaultHandlersTestPerAppType,
    testing::ValuesIn(FileManagerFileTaskPolicyDefaultHandlersTestPerAppType::
                          kAppIdPolicyIdMapping));

class FileManagerFileTaskPreferencesTest
    : public FileManagerFileTaskWithAppServiceTest {
 public:
  // Updates the default task preferences per the given dictionary values.
  // Used for testing ChooseAndSetDefaultTask.
  void UpdateDefaultTaskPreferences(const base::Value::Dict& mime_types,
                                    const base::Value::Dict& suffixes) {
    profile()->GetTestingPrefService()->SetDict(prefs::kDefaultTasksByMimeType,
                                                mime_types.Clone());
    profile()->GetTestingPrefService()->SetDict(prefs::kDefaultTasksBySuffix,
                                                suffixes.Clone());
  }  // namespace file_manager::file_tasks

  const base::Value::Dict& tasks_by_mime_type() {
    return profile()->GetTestingPrefService()->GetDict(
        prefs::kDefaultTasksByMimeType);
  }

  const base::Value::Dict& tasks_by_suffix() {
    return profile()->GetTestingPrefService()->GetDict(
        prefs::kDefaultTasksBySuffix);
  }

  void ClearPrefs() {
    profile()->GetTestingPrefService()->ClearPref(
        prefs::kDefaultTasksByMimeType);
    profile()->GetTestingPrefService()->ClearPref(prefs::kDefaultTasksBySuffix);
  }
};

// Test that the right task is chosen from multiple choices per mime types
// and file extensions.
TEST_F(FileManagerFileTaskPreferencesTest,
       ChooseAndSetDefaultTask_MultipleTasks) {
  // Text.app and Nice.app were found for "foo.txt".
  TaskDescriptor text_app_task("text-app-id", TASK_TYPE_FILE_HANDLER,
                               "action-id");
  TaskDescriptor nice_app_task("nice-app-id", TASK_TYPE_FILE_HANDLER,
                               "action-id");

  auto resulting_tasks = std::make_unique<ResultingTasks>();
  std::vector<FullTaskDescriptor>& tasks = resulting_tasks->tasks;

  tasks.emplace_back(
      text_app_task, "Text.app", GURL("http://example.com/text_app.png"),
      false /* is_default */, false /* is_generic_file_handler */,
      false /* is_file_extension_match */);
  tasks.emplace_back(
      nice_app_task, "Nice.app", GURL("http://example.com/nice_app.png"),
      false /* is_default */, false /* is_generic_file_handler */,
      false /* is_file_extension_match */);
  std::vector<extensions::EntryInfo> entries;
  entries.emplace_back(base::FilePath::FromUTF8Unsafe("foo.txt"), "text/plain",
                       false);

  // None of them should be chosen as default, as nothing is set in the
  // preferences.
  ChooseAndSetDefaultTask(profile(), entries, resulting_tasks.get());
  EXPECT_FALSE(tasks[0].is_default);
  EXPECT_FALSE(tasks[1].is_default);

  // Set Text.app as default for "text/plain" in the preferences.
  base::Value::Dict empty;
  base::Value::Dict mime_types;
  mime_types.Set("text/plain", base::Value(TaskDescriptorToId(text_app_task)));
  UpdateDefaultTaskPreferences(mime_types, empty);

  // Text.app should be chosen as default.
  ChooseAndSetDefaultTask(profile(), entries, resulting_tasks.get());
  EXPECT_TRUE(tasks[0].is_default);
  EXPECT_FALSE(tasks[1].is_default);

  // Change it back to non-default for testing further.
  tasks[0].is_default = false;

  // Clear the preferences and make sure none of them are default.
  UpdateDefaultTaskPreferences(empty, empty);
  ChooseAndSetDefaultTask(profile(), entries, resulting_tasks.get());
  EXPECT_FALSE(tasks[0].is_default);
  EXPECT_FALSE(tasks[1].is_default);

  // Set Nice.app as default for ".txt" in the preferences.
  base::Value::Dict suffixes;
  suffixes.Set(".txt", base::Value(TaskDescriptorToId(nice_app_task)));
  UpdateDefaultTaskPreferences(empty, suffixes);

  // Now Nice.app should be chosen as default.
  ChooseAndSetDefaultTask(profile(), entries, resulting_tasks.get());
  EXPECT_FALSE(tasks[0].is_default);
  EXPECT_TRUE(tasks[1].is_default);
}

// Test that internal file browser handler of the Files app is chosen as
// default even if nothing is set in the preferences.
TEST_F(FileManagerFileTaskPreferencesTest,
       ChooseAndSetDefaultTask_FallbackFileBrowser) {
  // The internal file browser handler of the Files app was found for
  // "foo.txt".
  TaskDescriptor files_app_task(
      kFileManagerAppId, TASK_TYPE_FILE_BROWSER_HANDLER, "view-in-browser");

  auto resulting_tasks = std::make_unique<ResultingTasks>();
  std::vector<FullTaskDescriptor>& tasks = resulting_tasks->tasks;

  tasks.emplace_back(
      files_app_task, "View in browser",
      GURL("http://example.com/some_icon.png"), false /* is_default */,
      false /* is_generic_file_handler */, false /* is_file_extension_match */);
  std::vector<extensions::EntryInfo> entries;
  entries.emplace_back(base::FilePath::FromUTF8Unsafe("foo.txt"), "text/plain",
                       false);

  // The internal file browser handler should be chosen as default, as it's a
  // fallback file browser handler.
  ChooseAndSetDefaultTask(profile(), entries, resulting_tasks.get());
  EXPECT_TRUE(tasks[0].is_default);
}

// Test that Text.app is chosen as default instead of the Files app
// even if nothing is set in the preferences.
TEST_F(FileManagerFileTaskPreferencesTest,
       ChooseAndSetDefaultTask_FallbackTextApp) {
  // Define the browser handler of the Files app for "foo.txt".
  TaskDescriptor files_app_task(
      kFileManagerAppId, TASK_TYPE_FILE_BROWSER_HANDLER, "view-in-browser");
  // Define the text editor app for "foo.txt".
  TaskDescriptor text_app_task(kTextEditorAppId, TASK_TYPE_FILE_HANDLER,
                               "Text");

  auto resulting_tasks = std::make_unique<ResultingTasks>();
  std::vector<FullTaskDescriptor>& tasks = resulting_tasks->tasks;

  tasks.emplace_back(
      files_app_task, "View in browser",
      GURL("http://example.com/some_icon.png"), false /* is_default */,
      false /* is_generic_file_handler */, false /* is_file_extension_match */);
  tasks.emplace_back(
      text_app_task, "Text",
      GURL("chrome://extension-icon/mmfbcljfglbokpmkimbfghdkjmjhdgbg/16/1"),
      false /* is_default */, false /* is_generic_file_handler */,
      false /* is_file_extension_match */);
  std::vector<extensions::EntryInfo> entries;
  entries.emplace_back(base::FilePath::FromUTF8Unsafe("foo.txt"), "text/plain",
                       false);

  // The text editor app should be chosen as default, as it's a fallback file
  // browser handler.
  ChooseAndSetDefaultTask(profile(), entries, resulting_tasks.get());
  EXPECT_TRUE(tasks[1].is_default);
}

// Test that browser is chosen as default for HTML files instead of the Text
// app even if nothing is set in the preferences.
TEST_F(FileManagerFileTaskPreferencesTest,
       ChooseAndSetDefaultTask_FallbackHtmlTextApp) {
  // Define the browser handler of the Files app for "foo.html".
  TaskDescriptor files_app_task(
      kFileManagerAppId, TASK_TYPE_FILE_BROWSER_HANDLER, "view-in-browser");
  // Define the text editor app for "foo.html".
  TaskDescriptor text_app_task(kTextEditorAppId, TASK_TYPE_FILE_HANDLER,
                               "Text");

  auto resulting_tasks = std::make_unique<ResultingTasks>();
  std::vector<FullTaskDescriptor>& tasks = resulting_tasks->tasks;

  tasks.emplace_back(
      files_app_task, "View in browser",
      GURL("http://example.com/some_icon.png"), false /* is_default */,
      false /* is_generic_file_handler */, false /* is_file_extension_match */);
  tasks.emplace_back(
      text_app_task, "Text",
      GURL("chrome://extension-icon/mmfbcljfglbokpmkimbfghdkjmjhdgbg/16/1"),
      false /* is_default */, false /* is_generic_file_handler */,
      false /* is_file_extension_match */);
  std::vector<extensions::EntryInfo> entries;
  entries.emplace_back(base::FilePath::FromUTF8Unsafe("foo.html"), "text/html",
                       false);

  // The internal file browser handler should be chosen as default,
  // as it's a fallback file browser handler.
  ChooseAndSetDefaultTask(profile(), entries, resulting_tasks.get());
  EXPECT_TRUE(tasks[0].is_default);
}

// Test that Office Editing is chosen as default even if nothing is set in the
// preferences.
TEST_F(FileManagerFileTaskPreferencesTest,
       ChooseAndSetDefaultTask_FallbackOfficeEditing) {
  // The Office Editing app was found for "slides.pptx".
  TaskDescriptor files_app_task(
      extension_misc::kQuickOfficeComponentExtensionId, TASK_TYPE_FILE_HANDLER,
      "Office Editing for Docs, Sheets & Slides");

  auto resulting_tasks = std::make_unique<ResultingTasks>();
  std::vector<FullTaskDescriptor>& tasks = resulting_tasks->tasks;

  tasks.emplace_back(
      files_app_task, "Office Editing for Docs, Sheets & Slides",
      GURL("chrome://extension-icon/bpmcpldpdmajfigpchkicefoigmkfalc/32/1"),
      false /* is_default */, false /* is_generic_file_handler */,
      false /* is_file_extension_match */);
  std::vector<extensions::EntryInfo> entries;
  entries.emplace_back(base::FilePath::FromUTF8Unsafe("slides.pptx"), "",
                       false);

  // The Office Editing app should be chosen as default, as it's a fallback
  // file browser handler.
  ChooseAndSetDefaultTask(profile(), entries, resulting_tasks.get());
  EXPECT_TRUE(tasks[0].is_default);
}

// Test that for changes of default app for PDF files, a metric is recorded.
TEST_F(FileManagerFileTaskPreferencesTest,
       UpdateDefaultTask_RecordsPdfDefaultAppChanges) {
  base::UserActionTester user_action_tester;

  // Non-PDF file types are not recorded.
  TaskDescriptor other_app_task("other-app-id", TASK_TYPE_FILE_HANDLER,
                                "action-id");
  UpdateDefaultTask(profile(), other_app_task, {".txt"}, {"text/plain"});
  // Even if it's the Media App.
  TaskDescriptor media_app_task(web_app::kMediaAppId, TASK_TYPE_FILE_HANDLER,
                                "action-id");
  UpdateDefaultTask(profile(), media_app_task, {"tiff"}, {"image/tiff"});

  EXPECT_EQ(0, user_action_tester.GetActionCount(
                   "MediaApp.PDF.DefaultApp.SwitchedAway"));
  EXPECT_EQ(0, user_action_tester.GetActionCount(
                   "MediaApp.PDF.DefaultApp.SwitchedTo"));

  // PDF files are recorded.
  UpdateDefaultTask(profile(), media_app_task, {".pdf"}, {"application/pdf"});

  EXPECT_EQ(1, user_action_tester.GetActionCount(
                   "MediaApp.PDF.DefaultApp.SwitchedTo"));
  EXPECT_EQ(0, user_action_tester.GetActionCount(
                   "MediaApp.PDF.DefaultApp.SwitchedAway"));
  user_action_tester.ResetCounts();

  UpdateDefaultTask(profile(), other_app_task, {".pdf"}, {"application/pdf"});

  EXPECT_EQ(0, user_action_tester.GetActionCount(
                   "MediaApp.PDF.DefaultApp.SwitchedTo"));
  EXPECT_EQ(1, user_action_tester.GetActionCount(
                   "MediaApp.PDF.DefaultApp.SwitchedAway"));
}

TEST_F(FileManagerFileTaskPreferencesTest,
       ChooseAndSetDefault_MatchesWithAlternateAppServiceTaskDescriptorForm) {
  std::string package = "com.example.gallery";
  std::string activity = "com.example.gallery.OpenActivity";
  std::string app_id = "zabcdefg";
  TaskType task_type = TASK_TYPE_ARC_APP;

  AddFakeAppToAppService(app_id, package, /*policy_ids=*/{},
                         apps::AppType::kArc);

  // Set the default app preference.
  std::string files_app_id = package + "/" + activity;
  TaskDescriptor file_task(files_app_id, task_type, "view");
  base::Value::Dict mime_types;
  mime_types.Set("image/png", base::Value(TaskDescriptorToId(file_task)));
  UpdateDefaultTaskPreferences(mime_types, {});

  // Create the file task descriptors to match against.
  TaskDescriptor app_service_file_task(app_id, task_type, activity);
  TaskDescriptor other_task("other", TASK_TYPE_FILE_BROWSER_HANDLER, "view");

  auto resulting_tasks = std::make_unique<ResultingTasks>();
  std::vector<FullTaskDescriptor>& tasks = resulting_tasks->tasks;

  tasks.emplace_back(
      app_service_file_task, "View Images",
      GURL("http://example.com/some_icon.png"), false /* is_default */,
      false /* is_generic_file_handler */, false /* is_file_extension_match */);
  tasks.emplace_back(other_task, "Other", GURL("http://example.com/other.text"),
                     false /* is_default */,
                     false /* is_generic_file_handler */,
                     false /* is_file_extension_match */);
  std::vector<extensions::EntryInfo> entries;
  entries.emplace_back(base::FilePath::FromUTF8Unsafe("foo.txt"), "image/png",
                       false);

  // Check if the correct task matched against the default preference.
  ChooseAndSetDefaultTask(profile(), entries, resulting_tasks.get());
  ASSERT_TRUE(tasks[0].is_default);
  ASSERT_FALSE(tasks[1].is_default);
}

TEST_F(FileManagerFileTaskPreferencesTest,
       UpdateDefaultTask_ConvertsArcAppServiceTaskDescriptorToStandardTaskId) {
  std::string package = "com.example.gallery";
  std::string activity = "com.example.gallery.OpenActivity";
  std::string app_id = "zabcdefg";
  TaskType task_type = TASK_TYPE_ARC_APP;
  std::string mime_type = "image/png";

  AddFakeAppToAppService(app_id, package, /*policy_ids=*/{},
                         apps::AppType::kArc);

  // Update default task preferences with our task descriptor (which is in the
  // format given from App Service file tasks).
  TaskDescriptor app_service_file_task(app_id, task_type, activity);
  UpdateDefaultTask(profile(), app_service_file_task, {}, {mime_type});

  // Check that the saved default preference is in the right format.
  std::string files_app_id = package + "/" + activity;
  std::string files_task_id = files_app_id + "|arc|view";
  const std::string* default_task_id =
      tasks_by_mime_type().FindString(mime_type);
  ASSERT_EQ(*default_task_id, files_task_id);
}

TEST_F(FileManagerFileTaskPreferencesTest, RemoveDefaultTask) {
  TaskDescriptor app1_view("app1", TASK_TYPE_FILE_BROWSER_HANDLER, "view");
  TaskDescriptor app1_edit("app1", TASK_TYPE_FILE_BROWSER_HANDLER, "edit");
  TaskDescriptor app2_view("app2", TASK_TYPE_FILE_BROWSER_HANDLER, "view");

  UpdateDefaultTask(profile(), app1_view, {"eXT1", "ext2"}, {"mime1", "mime2"});
  UpdateDefaultTask(profile(), app1_edit, {"Ext3"}, {"mime3"});
  UpdateDefaultTask(profile(), app2_view, {"ext4"}, {"mime4"});

  // Removing app1_edit or app2_view should not change app1_view.
  RemoveDefaultTask(profile(), app1_edit, {"ext1"}, {"mime1"});
  RemoveDefaultTask(profile(), app2_view, {"ext1"}, {"mime1"});
  EXPECT_EQ("app1|file|view", *tasks_by_suffix().FindString("ext1"));
  EXPECT_EQ("app1|file|view", *tasks_by_mime_type().FindString("mime1"));

  // Suffix match should be case-insensitive. Only specified suffixes or mimes
  // should be removed, others should not change.
  RemoveDefaultTask(profile(), app1_view, {"Ext1"}, {"mime1"});
  EXPECT_EQ(nullptr, tasks_by_suffix().FindString("ext1"));
  EXPECT_EQ(nullptr, tasks_by_mime_type().FindString("mime1"));
  EXPECT_EQ("app1|file|view", *tasks_by_suffix().FindString("ext2"));
  EXPECT_EQ("app1|file|view", *tasks_by_mime_type().FindString("mime2"));

  // Remove all matches for app1_view.
  RemoveDefaultTask(profile(), app1_view, {"ext1", "ext2"}, {"mime1", "mime2"});
  EXPECT_EQ(nullptr, tasks_by_suffix().FindString("ext1"));
  EXPECT_EQ(nullptr, tasks_by_suffix().FindString("ext2"));
  EXPECT_EQ(nullptr, tasks_by_mime_type().FindString("mime1"));
  EXPECT_EQ(nullptr, tasks_by_mime_type().FindString("mime2"));
}

TEST_F(FileManagerFileTaskPreferencesTest, UpdateDefaultTask_ReplaceExisting) {
  TaskDescriptor app1("app1", TASK_TYPE_FILE_BROWSER_HANDLER, "view");
  TaskDescriptor app2("app2", TASK_TYPE_FILE_BROWSER_HANDLER, "view");

  // Replace-existing true or false both work when no existing task exists.
  UpdateDefaultTask(profile(), app1, {"ext1"}, {"mime1"}, true);
  UpdateDefaultTask(profile(), app2, {"ext2"}, {"mime2"}, false);
  EXPECT_EQ("app1|file|view", *tasks_by_suffix().FindString("ext1"));
  EXPECT_EQ("app2|file|view", *tasks_by_suffix().FindString("ext2"));
  EXPECT_EQ("app1|file|view", *tasks_by_mime_type().FindString("mime1"));
  EXPECT_EQ("app2|file|view", *tasks_by_mime_type().FindString("mime2"));

  // Replace-existing true should overwrite, false should not.
  UpdateDefaultTask(profile(), app2, {"ext1"}, {"mime1"}, true);
  UpdateDefaultTask(profile(), app1, {"ext2"}, {"mime2"}, false);
  EXPECT_EQ("app2|file|view", *tasks_by_suffix().FindString("ext1"));
  EXPECT_EQ("app2|file|view", *tasks_by_suffix().FindString("ext2"));
  EXPECT_EQ("app2|file|view", *tasks_by_mime_type().FindString("mime1"));
  EXPECT_EQ("app2|file|view", *tasks_by_mime_type().FindString("mime2"));
}

}  // namespace file_manager::file_tasks