blob: 5325dc714fc6d013ae65457b2ab75e595e2012d0 [file] [log] [blame]
// 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 <memory>
#include <unordered_map>
#include "ash/constants/ash_features.h"
#include "ash/webui/file_manager/url_constants.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/ranges/algorithm.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "build/branding_buildflags.h"
#include "chrome/browser/apps/app_service/app_launch_params.h"
#include "chrome/browser/ash/drive/drivefs_test_support.h"
#include "chrome/browser/ash/file_manager/app_id.h"
#include "chrome/browser/ash/file_manager/file_manager_test_util.h"
#include "chrome/browser/ash/file_manager/file_tasks.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/file_manager/filesystem_api_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/volume_manager.h"
#include "chrome/browser/ash/file_system_provider/fake_extension_provider.h"
#include "chrome/browser/ash/file_system_provider/fake_provided_file_system.h"
#include "chrome/browser/ash/file_system_provider/provided_file_system_info.h"
#include "chrome/browser/ash/file_system_provider/provider_interface.h"
#include "chrome/browser/ash/file_system_provider/service.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/web_applications/web_app_launch_manager.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h"
#include "chrome/browser/ui/webui/ash/office_fallback/office_fallback_ui.h"
#include "chrome/browser/web_applications/test/profile_test_helper.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registry_update.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/ash/components/drivefs/fake_drivefs.h"
#include "chromeos/ash/components/drivefs/mojom/drivefs.mojom.h"
#include "components/drive/file_errors.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "extensions/browser/api/file_handlers/mime_util.h"
#include "extensions/browser/entry_info.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/constants.h"
#include "net/base/mime_util.h"
#include "services/network/test/test_network_connection_tracker.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "storage/browser/file_system/file_system_url.h"
#include "third_party/blink/public/common/features.h"
using web_app::kMediaAppId;
namespace file_manager {
namespace file_tasks {
namespace {
// A list of file extensions (`/` delimited) representing a selection of files
// and the app expected to be the default to open these files.
// A null app_id indicates there is no preferred default.
// A mime_type can be set to a result normally given by sniffing when
// net::GetMimeTypeFromFile() would not provide a result.
struct Expectation {
const char* file_extensions;
const char* app_id;
const char* mime_type = nullptr;
};
// Verifies that a single default task expectation (i.e. the expected
// default app to open a given set of file extensions) matches the default
// task in a vector of task descriptors. Decrements the provided |remaining|
// integer to provide additional verification that this function is invoked
// an expected number of times (i.e. even if the callback could be invoked
// asynchronously).
void VerifyTasks(int* remaining,
Expectation expectation,
std::unique_ptr<ResultingTasks> resulting_tasks) {
ASSERT_TRUE(resulting_tasks) << expectation.file_extensions;
--*remaining;
auto default_task = base::ranges::find_if(resulting_tasks->tasks,
&FullTaskDescriptor::is_default);
// Early exit for the uncommon situation where no default should be set.
if (!expectation.app_id) {
EXPECT_TRUE(default_task == resulting_tasks->tasks.end())
<< expectation.file_extensions;
return;
}
ASSERT_TRUE(default_task != resulting_tasks->tasks.end())
<< expectation.file_extensions;
EXPECT_EQ(expectation.app_id, default_task->task_descriptor.app_id)
<< " for extension: " << expectation.file_extensions;
// Verify no other task is set as default.
EXPECT_EQ(1, base::ranges::count_if(resulting_tasks->tasks,
&FullTaskDescriptor::is_default))
<< expectation.file_extensions;
}
// Helper to quit a run loop after invoking VerifyTasks().
void VerifyAsyncTask(int* remaining,
Expectation expectation,
base::OnceClosure quit_closure,
std::unique_ptr<ResultingTasks> resulting_tasks) {
VerifyTasks(remaining, expectation, std::move(resulting_tasks));
std::move(quit_closure).Run();
}
// Installs a chrome app that handles .tiff.
scoped_refptr<const extensions::Extension> InstallTiffHandlerChromeApp(
Profile* profile) {
return test::InstallTestingChromeApp(
profile, "extensions/api_test/file_browser/app_file_handler");
}
class FileTasksBrowserTest : public TestProfileTypeMixin<InProcessBrowserTest> {
public:
void SetUpOnMainThread() override {
test::AddDefaultComponentExtensionsOnMainThread(browser()->profile());
ash::SystemWebAppManager::GetForTest(browser()->profile())
->InstallSystemAppsForTesting();
}
// Tests that each of the passed expectations open by default in the expected
// app.
void TestExpectationsAgainstDefaultTasks(
const std::vector<Expectation>& expectations) {
int remaining = expectations.size();
const base::FilePath prefix = base::FilePath().AppendASCII("file");
for (const Expectation& test : expectations) {
std::vector<extensions::EntryInfo> entries;
std::vector<GURL> file_urls;
std::vector<base::StringPiece> all_extensions =
base::SplitStringPiece(test.file_extensions, "/",
base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
for (base::StringPiece extension : all_extensions) {
base::FilePath path = prefix.AddExtension(extension);
std::string mime_type;
net::GetMimeTypeFromFile(path, &mime_type);
if (test.mime_type != nullptr) {
// Sniffing isn't used when GetMimeTypeFromFile() succeeds, so there
// shouldn't be a hard-coded mime type configured.
EXPECT_TRUE(mime_type.empty())
<< "Did not expect mime match " << mime_type << " for " << path;
mime_type = test.mime_type;
} else {
EXPECT_FALSE(mime_type.empty()) << "No mime type for " << path;
}
entries.emplace_back(path, mime_type, false);
GURL url = GURL(base::JoinString(
{"filesystem:https://site.com/isolated/foo.", extension}, ""));
ASSERT_TRUE(url.is_valid());
file_urls.push_back(url);
}
// task_verifier callback is invoked synchronously from
// FindAllTypesOfTasks.
FindAllTypesOfTasks(browser()->profile(), entries, file_urls,
base::BindOnce(&VerifyTasks, &remaining, test));
}
EXPECT_EQ(0, remaining);
}
private:
base::test::ScopedFeatureList feature_list_{
blink::features::kFileHandlingAPI};
};
} // namespace
// Changes to the following tests may have implications for file handling
// declarations in built-in app manifests, because logic in
// ChooseAndSetDefaultTask() treats handlers for extensions with a higher
// priority than handlers for mime types. Provide MIME types here for extensions
// known to be missing mime types from net::GetMimeTypeFromFile() (see
// ExtensionToMimeMapping test). In practice, these MIME types are populated via
// file sniffing, but tests in this file do not operate on real files. We hard
// code MIME types that file sniffing obtained experimentally from sample files.
// Test file extensions correspond to mime types where expected.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, ExtensionToMimeMapping) {
constexpr struct {
const char* file_extension;
bool has_mime = true;
} kExpectations[] = {
// Images.
{"bmp"},
{"gif"},
{"ico"},
{"jpg"},
{"jpeg"},
{"png"},
{"webp"},
// Raw.
{"arw"},
{"cr2"},
{"dng"},
{"nef"},
{"nrw"},
{"orf"},
{"raf"},
{"rw2"},
// Video.
{"3gp"},
{"avi"},
{"m4v"},
{"mkv"},
{"mov"},
{"mp4"},
{"mpeg"},
{"mpeg4", false},
{"mpg"},
{"mpg4", false},
{"ogm"},
{"ogv"},
{"ogx"},
{"webm"},
// Audio.
{"amr"},
{"flac"},
{"m4a"},
{"mp3"},
{"oga"},
{"ogg"},
{"wav"},
};
const base::FilePath prefix = base::FilePath().AppendASCII("file");
std::string mime_type;
for (const auto& test : kExpectations) {
base::FilePath path = prefix.AddExtension(test.file_extension);
EXPECT_EQ(test.has_mime, net::GetMimeTypeFromFile(path, &mime_type))
<< test.file_extension;
}
}
// Tests the default handlers for various file types in ChromeOS. This test
// exists to ensure the default app that launches when you open a file in the
// ChromeOS file manager does not change unexpectedly. Multiple default apps are
// allowed to register a handler for the same file type. Without that, it is not
// possible for an app to open that type even when given explicit direction via
// the chrome.fileManagerPrivate.executeTask API. The current conflict
// resolution mechanism is "sort by extension ID", which has the desired result.
// If desires change, we'll need to update ChooseAndSetDefaultTask() with some
// additional logic.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, ImageHandlerChangeDetector) {
std::vector<Expectation> expectations = {
// Images.
{"bmp", kMediaAppId},
{"gif", kMediaAppId},
{"ico", kMediaAppId},
{"jpg", kMediaAppId},
{"jpeg", kMediaAppId},
{"png", kMediaAppId},
{"webp", kMediaAppId},
// Raw (handled by MediaApp).
{"arw", kMediaAppId},
{"cr2", kMediaAppId},
{"dng", kMediaAppId},
{"nef", kMediaAppId},
{"nrw", kMediaAppId},
{"orf", kMediaAppId},
{"raf", kMediaAppId},
{"rw2", kMediaAppId},
{"NRW", kMediaAppId}, // Uppercase extension.
};
TestExpectationsAgainstDefaultTasks(expectations);
}
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, VideoHandlerChangeDetector) {
std::vector<Expectation> expectations = {
{"3gp", kMediaAppId}, {"avi", kMediaAppId},
{"m4v", kMediaAppId}, {"mkv", kMediaAppId},
{"mov", kMediaAppId}, {"mp4", kMediaAppId},
{"mpeg", kMediaAppId}, {"mpeg4", kMediaAppId, "video/mpeg"},
{"mpg", kMediaAppId}, {"mpg4", kMediaAppId, "video/mpeg"},
{"ogm", kMediaAppId}, {"ogv", kMediaAppId},
{"ogx", kMediaAppId}, {"webm", kMediaAppId},
};
TestExpectationsAgainstDefaultTasks(expectations);
}
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, AudioHandlerChangeDetector) {
std::vector<Expectation> expectations = {
{"flac", kMediaAppId}, {"m4a", kMediaAppId}, {"mp3", kMediaAppId},
{"oga", kMediaAppId}, {"ogg", kMediaAppId}, {"wav", kMediaAppId},
};
TestExpectationsAgainstDefaultTasks(expectations);
}
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, PdfHandlerChangeDetector) {
std::vector<Expectation> expectations = {{"pdf", kMediaAppId},
{"PDF", kMediaAppId}};
TestExpectationsAgainstDefaultTasks(expectations);
}
// Spot test the default handlers for selections that include multiple different
// file types. Only tests combinations of interest to the Media App.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, MultiSelectDefaultHandler) {
std::vector<Expectation> expectations = {
{"jpg/gif", kMediaAppId},
{"jpg/mp4", kMediaAppId},
};
TestExpectationsAgainstDefaultTasks(expectations);
}
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
// Check that QuickOffice has a handler installed for common Office doc types.
// This test only runs with the is_chrome_branded GN flag set because otherwise
// QuickOffice is not installed.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, QuickOffice) {
std::vector<Expectation> expectations = {
{"doc", extension_misc::kQuickOfficeComponentExtensionId},
{"docx", extension_misc::kQuickOfficeComponentExtensionId},
{"ppt", extension_misc::kQuickOfficeComponentExtensionId},
{"pptx", extension_misc::kQuickOfficeComponentExtensionId},
{"xls", extension_misc::kQuickOfficeComponentExtensionId},
{"xlsx", extension_misc::kQuickOfficeComponentExtensionId},
};
TestExpectationsAgainstDefaultTasks(expectations);
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
// The Media App will be preferred over a chrome app with a specific extension,
// unless that app is set default via prefs.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, MediaAppPreferredOverChromeApps) {
if (profile_type() == TestProfileType::kGuest) {
// The provided file system can't install in guest mode. Just check that
// MediaApp handles tiff.
TestExpectationsAgainstDefaultTasks({{"tiff", kMediaAppId}});
return;
}
Profile* profile = browser()->profile();
auto extension = InstallTiffHandlerChromeApp(profile);
TestExpectationsAgainstDefaultTasks({{"tiff", kMediaAppId}});
UpdateDefaultTask(
profile,
TaskDescriptor(extension->id(), StringToTaskType("app"), "tiffAction"),
{"tiff"}, {"image/tiff"});
if (profile_type() == TestProfileType::kIncognito) {
// In incognito, the installed app is not enabled and we filter it out.
TestExpectationsAgainstDefaultTasks({{"tiff", kMediaAppId}});
} else {
TestExpectationsAgainstDefaultTasks({{"tiff", extension->id().c_str()}});
}
}
// Test expectations for files coming from provided file systems.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, ProvidedFileSystemFileSource) {
if (profile_type() == TestProfileType::kGuest) {
// Provided file systems don't exist in guest.
return;
}
// The current test expectation: a GIF file in the provided file system called
// "readwrite.gif" should open with the MediaApp.
const char kTestFile[] = "readwrite.gif";
Expectation test = {"gif", kMediaAppId};
int remaining_expectations = 1;
Profile* profile = browser()->profile();
base::WeakPtr<Volume> volume =
test::InstallFileSystemProviderChromeApp(profile);
GURL url;
ASSERT_TRUE(util::ConvertAbsoluteFilePathToFileSystemUrl(
profile, volume->mount_path().AppendASCII(kTestFile),
util::GetFileManagerURL(), &url));
// Note |url| differs slightly to the result of ToGURL() below. The colons
// either side of `:test-image-provider-fs:` become escaped as `%3A`.
storage::FileSystemURL filesystem_url =
util::GetFileManagerFileSystemContext(profile)
->CrackURLInFirstPartyContext(url);
std::vector<GURL> urls = {filesystem_url.ToGURL()};
std::vector<extensions::EntryInfo> entries;
// We could add the mime type here, but since a "real" file is provided, we
// can get additional coverage of the mime determination. For non-native files
// this uses metadata only (not sniffing).
entries.emplace_back(filesystem_url.path(), "", false);
base::RunLoop run_loop;
auto verifier = base::BindOnce(&VerifyAsyncTask, &remaining_expectations,
test, run_loop.QuitClosure());
extensions::app_file_handler_util::GetMimeTypeForLocalPath(
profile, entries[0].path,
base::BindLambdaForTesting([&](const std::string& mime_type) {
entries[0].mime_type = mime_type;
EXPECT_EQ(entries[0].mime_type, "image/gif");
FindAllTypesOfTasks(profile, entries, urls, std::move(verifier));
}));
run_loop.Run();
EXPECT_EQ(remaining_expectations, 0);
}
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, ExecuteWebApp) {
auto web_app_info = std::make_unique<WebAppInstallInfo>();
web_app_info->start_url = GURL("https://www.example.com/");
web_app_info->scope = GURL("https://www.example.com/");
apps::FileHandler handler;
handler.action = GURL("https://www.example.com/handle_file");
handler.display_name = u"activity name";
apps::FileHandler::AcceptEntry accept_entry1;
accept_entry1.mime_type = "image/jpeg";
accept_entry1.file_extensions.insert(".jpeg");
handler.accept.push_back(accept_entry1);
apps::FileHandler::AcceptEntry accept_entry2;
accept_entry2.mime_type = "image/png";
accept_entry2.file_extensions.insert(".png");
handler.accept.push_back(accept_entry2);
web_app_info->file_handlers.push_back(std::move(handler));
Profile* const profile = browser()->profile();
TaskDescriptor task_descriptor;
if (GetParam().crosapi_state == TestProfileParam::CrosapiParam::kDisabled) {
// Install a PWA in ash.
web_app::AppId app_id =
web_app::test::InstallWebApp(profile, std::move(web_app_info));
task_descriptor = TaskDescriptor(app_id, TaskType::TASK_TYPE_WEB_APP,
"https://www.example.com/handle_file");
// Skip past the permission dialog.
web_app::WebAppProvider::GetForTest(profile)
->sync_bridge()
.SetAppFileHandlerApprovalState(app_id,
web_app::ApiApprovalState::kAllowed);
} else {
// Use an existing SWA in ash - Media app.
task_descriptor = TaskDescriptor(kMediaAppId, TaskType::TASK_TYPE_WEB_APP,
"chrome://media-app/open");
// TODO(petermarshall): Install the web app in Lacros once installing and
// launching apps from ash -> lacros is possible.
}
base::RunLoop run_loop;
web_app::WebAppLaunchManager::SetOpenApplicationCallbackForTesting(
base::BindLambdaForTesting(
[&run_loop](apps::AppLaunchParams&& params) -> content::WebContents* {
if (GetParam().crosapi_state ==
TestProfileParam::CrosapiParam::kDisabled) {
EXPECT_EQ(params.override_url,
"https://www.example.com/handle_file");
} else {
EXPECT_EQ(params.override_url, "chrome://media-app/open");
}
EXPECT_EQ(params.launch_files.size(), 2U);
EXPECT_TRUE(base::EndsWith(params.launch_files.at(0).MaybeAsASCII(),
"foo.jpeg"));
EXPECT_TRUE(base::EndsWith(params.launch_files.at(1).MaybeAsASCII(),
"bar.png"));
run_loop.Quit();
return nullptr;
}));
base::FilePath file1 =
util::GetMyFilesFolderForProfile(profile).AppendASCII("foo.jpeg");
base::FilePath file2 =
util::GetMyFilesFolderForProfile(profile).AppendASCII("bar.png");
GURL url1;
CHECK(util::ConvertAbsoluteFilePathToFileSystemUrl(
profile, file1, util::GetFileManagerURL(), &url1));
GURL url2;
CHECK(util::ConvertAbsoluteFilePathToFileSystemUrl(
profile, file2, util::GetFileManagerURL(), &url2));
std::vector<storage::FileSystemURL> files;
files.push_back(storage::FileSystemURL::CreateForTest(url1));
files.push_back(storage::FileSystemURL::CreateForTest(url2));
ExecuteFileTask(profile, task_descriptor, files, base::DoNothing());
run_loop.Run();
}
// Launch a Chrome app with a real file and wait for it to ping back.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, ExecuteChromeApp) {
if (profile_type() == TestProfileType::kGuest) {
// The app can't install in guest mode.
return;
}
Profile* const profile = browser()->profile();
auto extension = InstallTiffHandlerChromeApp(profile);
TaskDescriptor task_descriptor(extension->id(), TASK_TYPE_FILE_HANDLER,
"tiffAction");
base::FilePath path;
EXPECT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &path));
path = path.AppendASCII("chromeos/file_manager/test_small.tiff");
{
base::ScopedAllowBlockingForTesting allow_blocking;
EXPECT_TRUE(base::PathExists(path));
}
// Copy the file into My Files.
file_manager::test::FolderInMyFiles folder(profile);
folder.Add({path});
base::FilePath path_in_my_files = folder.files()[0];
GURL tiff_url;
CHECK(util::ConvertAbsoluteFilePathToFileSystemUrl(
profile, path_in_my_files, util::GetFileManagerURL(), &tiff_url));
std::vector<storage::FileSystemURL> files;
files.push_back(storage::FileSystemURL::CreateForTest(tiff_url));
content::DOMMessageQueue message_queue;
ExecuteFileTask(profile, task_descriptor, files, base::DoNothing());
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
ASSERT_EQ("\"Received tiffAction with: test_small.tiff\"", message);
}
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, IsExtensionInstalled) {
if (profile_type() == TestProfileType::kGuest) {
// The extension can't install in guest mode.
return;
}
Profile* const profile = browser()->profile();
// Install new extension.
auto extension = InstallTiffHandlerChromeApp(profile);
ASSERT_TRUE(IsExtensionInstalled(profile, extension->id()));
extensions::ExtensionRegistry* registry =
extensions::ExtensionRegistry::Get(profile);
// Uninstall extension.
registry->RemoveEnabled(extension->id());
ASSERT_FALSE(IsExtensionInstalled(profile, extension->id()));
}
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
// This test only runs with the is_chrome_branded GN flag set because otherwise
// QuickOffice is not installed.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, IsExtensionInstalledQuickOffice) {
Profile* const profile = browser()->profile();
ASSERT_TRUE(IsExtensionInstalled(
profile, extension_misc::kQuickOfficeComponentExtensionId));
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
const TaskDescriptor CreateWebDriveOfficeTask() {
// The SWA actionId is prefixed with chrome://file-manager/?ACTION_ID.
const std::string& full_action_id =
base::StrCat({ash::file_manager::kChromeUIFileManagerURL, "?",
kActionIdWebDriveOfficeWord});
return TaskDescriptor(kFileManagerSwaAppId, TASK_TYPE_WEB_APP,
full_action_id);
}
const TaskDescriptor CreateOpenInOfficeTask() {
// The SWA actionId is prefixed with chrome://file-manager/?ACTION_ID.
const std::string& full_action_id = base::StrCat(
{ash::file_manager::kChromeUIFileManagerURL, "?", kActionIdOpenInOffice});
return TaskDescriptor(kFileManagerSwaAppId, TASK_TYPE_WEB_APP,
full_action_id);
}
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
// This test only runs with the is_chrome_branded GN flag set because otherwise
// QuickOffice is not installed.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, FallbackFailsNoQuickOffice) {
storage::FileSystemURL test_url;
Profile* const profile = browser()->profile();
extensions::ExtensionRegistry* registry =
extensions::ExtensionRegistry::Get(profile);
const extensions::Extension* quick_office = registry->GetInstalledExtension(
extension_misc::kQuickOfficeComponentExtensionId);
// Uninstall QuickOffice.
registry->RemoveEnabled(extension_misc::kQuickOfficeComponentExtensionId);
// GetUserFallbackChoice() returns `False` because QuickOffice is not
// installed.
ASSERT_FALSE(
GetUserFallbackChoice(profile, CreateWebDriveOfficeTask(), {test_url},
ash::office_fallback::FallbackReason::kOffline));
// Install QuickOffice.
registry->AddEnabled(quick_office);
// GetUserFallbackChoice() returns `True` because QuickOffice is installed.
ASSERT_TRUE(
GetUserFallbackChoice(profile, CreateWebDriveOfficeTask(), {test_url},
ash::office_fallback::FallbackReason::kOffline));
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
// TODO(cassycc): move this class to a more appropriate spot.
// Fake DriveFs specific to the `OfficeFallbackDriveTest`. Allows a test file to
// be "added" to the DriveFs via `SetMetadata()`. The `alternate_url` of the
// file can be retrieved via `GetMetadata()`. This a simplified version of
// `FakeDriveFs` because the only condition for the file to be in the DriveFs is
// to have a `alternate_url_` entry.
class FakeSimpleDriveFs : public drivefs::FakeDriveFs {
public:
explicit FakeSimpleDriveFs(const base::FilePath& mount_path)
: drivefs::FakeDriveFs(mount_path) {}
// Sets `alternate_url_` which is retrieved later in `GetMetadata()`.
void SetMetadata(const base::FilePath& path,
const std::string& alternate_url) {
alternate_url_[path] = alternate_url;
}
private:
// drivefs::mojom::DriveFs:
// This is a simplified version of `FakeDriveFs::SetMetadata()` that just
// returns a default `metadata` with the alternate_url set in `SetMetadata`.
void GetMetadata(const base::FilePath& path,
GetMetadataCallback callback) override {
auto metadata = drivefs::mojom::FileMetadata::New();
metadata->alternate_url = alternate_url_[path];
// Fill the rest of `metadata` with default values.
metadata->content_mime_type = "";
const drivefs::mojom::Capabilities& capabilities = {};
metadata->capabilities = capabilities.Clone();
metadata->folder_feature = {};
metadata->available_offline = false;
metadata->shared = false;
std::move(callback).Run(drive::FILE_ERROR_OK, std::move(metadata));
}
// Each file in this DriveFs has an entry.
std::unordered_map<base::FilePath, std::string> alternate_url_;
};
// TODO(cassycc): move this class to a more appropriate spot
// Fake DriveFs helper specific to the `OfficeFallbackDriveTest`. Implements the
// functions to create a `FakeSimpleDriveFs`.
class FakeSimpleDriveFsHelper : public drive::FakeDriveFsHelper {
public:
FakeSimpleDriveFsHelper(Profile* profile, const base::FilePath& mount_path)
: drive::FakeDriveFsHelper(profile, mount_path),
mount_path_(mount_path),
fake_drivefs_(mount_path_) {}
base::RepeatingCallback<std::unique_ptr<drivefs::DriveFsBootstrapListener>()>
CreateFakeDriveFsListenerFactory() {
return base::BindRepeating(&drivefs::FakeDriveFs::CreateMojoListener,
base::Unretained(&fake_drivefs_));
}
const base::FilePath& mount_path() { return mount_path_; }
FakeSimpleDriveFs& fake_drivefs() { return fake_drivefs_; }
private:
const base::FilePath mount_path_;
FakeSimpleDriveFs fake_drivefs_;
};
// Tests the office fallback flow that occurs when a user fails to open an
// office file from Drive.
class OfficeFallbackDriveTest : public InProcessBrowserTest {
public:
OfficeFallbackDriveTest() {
feature_list_.InitAndEnableFeature(ash::features::kUploadOfficeToCloud);
EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
drive_mount_point_ = temp_dir_.GetPath();
test_file_name_ = "text.docx";
// Path of test file relative to the DriveFs mount point.
relative_test_file_path = base::FilePath("/").AppendASCII(test_file_name_);
}
OfficeFallbackDriveTest(const OfficeFallbackDriveTest&) = delete;
OfficeFallbackDriveTest& operator=(const OfficeFallbackDriveTest&) = delete;
void SetUpInProcessBrowserTestFixture() override {
// Setup drive integration service.
create_drive_integration_service_ = base::BindRepeating(
&OfficeFallbackDriveTest::CreateDriveIntegrationService,
base::Unretained(this));
service_factory_for_test_ = std::make_unique<
drive::DriveIntegrationServiceFactory::ScopedFactoryForTest>(
&create_drive_integration_service_);
}
void TearDown() override {
InProcessBrowserTest::TearDown();
storage::ExternalMountPoints::GetSystemInstance()->RevokeAllFileSystems();
}
drive::DriveIntegrationService* CreateDriveIntegrationService(
Profile* profile) {
base::ScopedAllowBlockingForTesting allow_blocking;
fake_drivefs_helpers_[profile] =
std::make_unique<FakeSimpleDriveFsHelper>(profile, drive_mount_point_);
auto* integration_service = new drive::DriveIntegrationService(
profile, "", drive_mount_point_,
fake_drivefs_helpers_[profile]->CreateFakeDriveFsListenerFactory());
return integration_service;
}
Profile* profile() { return browser()->profile(); }
mojo::Remote<drivefs::mojom::DriveFsDelegate>& drivefs_delegate() {
return fake_drivefs_helpers_[profile()]->fake_drivefs().delegate();
}
base::FilePath observed_absolute_drive_path() {
return base::FilePath(
(drive_mount_point_.value() + relative_test_file_path.value()));
}
void SetConnectionOnline() {
network_connection_tracker_ =
network::TestNetworkConnectionTracker::CreateInstance();
content::SetNetworkConnectionTrackerForTesting(nullptr);
content::SetNetworkConnectionTrackerForTesting(
network_connection_tracker_.get());
network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
network::mojom::ConnectionType::CONNECTION_WIFI);
}
// Complete the set up of the fake DriveFs with a test file added.
void SetUpTest() {
// Install QuickOffice for the check in GetUserFallbackChoice() before
// the dialog can launched.
test::AddDefaultComponentExtensionsOnMainThread(profile());
// Create Drive root directory.
{
base::ScopedAllowBlockingForTesting allow_blocking;
EXPECT_TRUE(base::CreateDirectory(drive_mount_point_));
}
// Add test file to the DriveFs.
fake_drivefs_helpers_[profile()]->fake_drivefs().SetMetadata(
relative_test_file_path, alternate_url_);
// Get URL for test file in the DriveFs.
drive_test_file_url_ = ash::cloud_upload::FilePathToFileSystemURL(
profile(),
file_manager::util::GetFileManagerFileSystemContext(profile()),
observed_absolute_drive_path());
}
protected:
const std::string alternate_url_ =
"https://docs.google.com/document/d/smalldocxid?rtpof=true&usp=drive_fs";
FileSystemURL drive_test_file_url_;
private:
base::ScopedTempDir temp_dir_;
base::FilePath drive_mount_point_;
std::string test_file_name_;
base::FilePath relative_test_file_path;
std::unique_ptr<network::TestNetworkConnectionTracker>
network_connection_tracker_;
base::test::ScopedFeatureList feature_list_;
drive::DriveIntegrationServiceFactory::FactoryCallback
create_drive_integration_service_;
std::unique_ptr<drive::DriveIntegrationServiceFactory::ScopedFactoryForTest>
service_factory_for_test_;
std::map<Profile*, std::unique_ptr<FakeSimpleDriveFsHelper>>
fake_drivefs_helpers_;
};
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
// Test to check that the test file fails to open when the system is offline but
// is successfully opened with a "try-again" dialog choice after the
// systems comes online.
IN_PROC_BROWSER_TEST_F(OfficeFallbackDriveTest, DriveTryAgain) {
// Add test file to fake DriveFs.
SetUpTest();
const TaskDescriptor web_drive_office_task = CreateWebDriveOfficeTask();
std::vector<storage::FileSystemURL> file_urls{drive_test_file_url_};
// Watch for dialog URL chrome://office-fallback.
GURL expected_dialog_URL(chrome::kChromeUIOfficeFallbackURL);
content::TestNavigationObserver navigation_observer_dialog(
expected_dialog_URL);
navigation_observer_dialog.StartWatchingNewWebContents();
// Fails as system is offline and thus will open dialog.
ExecuteFileTask(
profile(), web_drive_office_task, file_urls,
base::BindOnce(
[](extensions::api::file_manager_private::TaskResult result,
std::string error_message) {}));
// Wait for dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
SetConnectionOnline();
// Start watching for the opening of `expected_web_drive_office_url`. The
// query parameter is concatenated to the URL as office files opened from
// drive have this query parameter added (https://crrev.com/c/3867338).
GURL expected_web_drive_office_url(alternate_url_ + "&cros_files=true");
content::TestNavigationObserver navigation_observer_office(
expected_web_drive_office_url);
navigation_observer_office.StartWatchingNewWebContents();
// Run dialog callback, simulate user choosing to "try-again". Will succeed
// because system is online.
OnDialogChoiceReceived(profile(), web_drive_office_task, file_urls,
ash::office_fallback::kDialogChoiceTryAgain);
// Wait for file to open in web drive office.
navigation_observer_office.Wait();
}
// TODO(cassycc): move this class to a more appropriate spot
// Fake provided file system implementation specific to the
// `OfficeFallbackOneDriveTest`. Notifies the `OfficeFallbackOneDriveTest` upon
// the "OPEN_WEB" action on the file system.
class FakeProvidedFileSystemOneDrive
: public ash::file_system_provider::FakeProvidedFileSystem {
public:
FakeProvidedFileSystemOneDrive(
const ash::file_system_provider::ProvidedFileSystemInfo& file_system_info,
base::OnceClosure callback)
: FakeProvidedFileSystem(file_system_info),
callback_(std::move(callback)) {}
ash::file_system_provider::AbortCallback ExecuteAction(
const std::vector<base::FilePath>& entry_paths,
const std::string& action_id,
storage::AsyncFileUtil::StatusCallback callback) override {
// When the "OPEN_WEB" action is observed, notify the
// `OfficeFallbackOneDriveTest` via the `callback_`.
if (action_id == file_manager::file_tasks::kActionIdOpenWeb) {
std::move(callback_).Run();
}
return FakeProvidedFileSystem::ExecuteAction(entry_paths, action_id,
std::move(callback));
}
protected:
// OfficeFallbackOneDriveTest::OpenWebAction.
base::OnceClosure callback_;
};
// TODO(cassycc): move this class to a more appropriate spot
// Fake extension provider specific to the `OfficeFallbackOneDriveTest`.
// Implements the functions to create a `FakeProvidedFileSystemOneDrive` with a
// test file added and passes along the appropriate `callback`.
class FakeExtensionProviderOneDrive
: public ash::file_system_provider::FakeExtensionProvider {
public:
static std::unique_ptr<ProviderInterface> Create(
const extensions::ExtensionId& extension_id,
const base::FilePath relative_test_file_path,
std::string test_file_name,
base::OnceClosure callback) {
ash::file_system_provider::Capabilities default_capabilities(
false, false, false, extensions::SOURCE_NETWORK);
return std::unique_ptr<ProviderInterface>(new FakeExtensionProviderOneDrive(
extension_id, default_capabilities, relative_test_file_path,
test_file_name, std::move(callback)));
}
std::unique_ptr<ash::file_system_provider::ProvidedFileSystemInterface>
CreateProvidedFileSystem(
Profile* profile,
const ash::file_system_provider::ProvidedFileSystemInfo& file_system_info)
override {
DCHECK(profile);
std::unique_ptr<FakeProvidedFileSystemOneDrive> fake_provided_file_system =
std::make_unique<FakeProvidedFileSystemOneDrive>(file_system_info,
std::move(callback_));
// Add test file.
fake_provided_file_system->AddEntry(
relative_test_file_path_, false, test_file_name_, 0, base::Time::Now(),
"application/"
"vnd.openxmlformats-officedocument.wordprocessingml.document",
"");
return fake_provided_file_system;
}
private:
FakeExtensionProviderOneDrive(
const extensions::ExtensionId& extension_id,
const ash::file_system_provider::Capabilities& capabilities,
const base::FilePath relative_test_file_path,
std::string test_file_name,
base::OnceClosure callback)
: FakeExtensionProvider(extension_id, capabilities),
callback_(std::move(callback)),
relative_test_file_path_(relative_test_file_path),
test_file_name_(test_file_name) {}
// OfficeFallbackOneDriveTest::OpenWebAction.
base::OnceClosure callback_;
const base::FilePath relative_test_file_path_;
std::string test_file_name_;
};
// Tests the office fallback flow that occurs when a user fails to open an
// office file from OneDrive.
class OfficeFallbackOneDriveTest : public InProcessBrowserTest {
public:
OfficeFallbackOneDriveTest() {
feature_list_.InitAndEnableFeature(ash::features::kUploadOfficeToCloud);
test_file_name_ = "text.docx";
relative_test_file_path_ = base::FilePath(test_file_name_);
file_system_id_ = "odfs";
}
OfficeFallbackOneDriveTest(const OfficeFallbackOneDriveTest&) = delete;
OfficeFallbackOneDriveTest& operator=(const OfficeFallbackOneDriveTest&) =
delete;
void TearDown() override {
InProcessBrowserTest::TearDown();
storage::ExternalMountPoints::GetSystemInstance()->RevokeAllFileSystems();
}
// Callback for when the `FakeProvidedFileSystemOneDrive` observes that a file
// in the ODFS was opened.
void OpenWebAction() { file_opened_ = true; }
// Creates and mounts fake provided file system for OneDrive with a test file
// added. Installs QuickOffice for the check in GetUserFallbackChoice() before
// the dialog can launched.
void SetUpTest() {
// Install QuickOffice.
test::AddDefaultComponentExtensionsOnMainThread(browser()->profile());
service_ = ash::file_system_provider::Service::Get(profile());
// Set `OfficeFallbackOneDriveTest::OpenWebAction` as the callback for the
// `FakeProvidedFileSystemOneDrive`. The use of `base::Unretained()` is safe
// because the class will exist for the duration of the test.
service_->RegisterProvider(FakeExtensionProviderOneDrive::Create(
kODFSExtensionId, relative_test_file_path_, test_file_name_,
base::BindOnce(&OfficeFallbackOneDriveTest::OpenWebAction,
base::Unretained(this))));
provider_id_ = ash::file_system_provider::ProviderId::CreateFromExtensionId(
kODFSExtensionId);
ash::file_system_provider::MountOptions options(file_system_id_, "ODFS");
EXPECT_EQ(base::File::FILE_OK,
service_->MountFileSystem(provider_id_, options));
// Get URL for test file in OneDrive.
one_drive_test_file_url_ = ash::cloud_upload::FilePathToFileSystemURL(
profile(),
file_manager::util::GetFileManagerFileSystemContext(profile()),
observed_one_drive_path());
}
Profile* profile() { return browser()->profile(); }
base::FilePath observed_one_drive_path() {
std::vector<ash::file_system_provider::ProvidedFileSystemInfo>
file_systems = service_->GetProvidedFileSystemInfoList(provider_id_);
// One and only one filesystem should be mounted for the ODFS extension.
EXPECT_EQ(file_systems.size(), 1u);
base::FilePath observed_one_drive_path =
file_systems[0]
.mount_path()
.Append(ash::cloud_upload::kDestinationFolder)
.Append(relative_test_file_path_);
return observed_one_drive_path;
}
void SetConnectionOnline() {
network_connection_tracker_ =
network::TestNetworkConnectionTracker::CreateInstance();
content::SetNetworkConnectionTrackerForTesting(nullptr);
content::SetNetworkConnectionTrackerForTesting(
network_connection_tracker_.get());
network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
network::mojom::ConnectionType::CONNECTION_WIFI);
}
protected:
bool file_opened_;
FileSystemURL one_drive_test_file_url_;
private:
base::test::ScopedFeatureList feature_list_;
std::string file_system_id_;
std::unique_ptr<network::TestNetworkConnectionTracker>
network_connection_tracker_;
ash::file_system_provider::ProviderId provider_id_;
base::FilePath relative_test_file_path_;
ash::file_system_provider::Service* service_;
std::string test_file_name_;
};
// Test to check that the test file fails to open when the system is offline but
// is successfully opened with a "try-again" dialog choice after the
// systems comes online.
IN_PROC_BROWSER_TEST_F(OfficeFallbackOneDriveTest, OneDriveTryAgain) {
// Creates a fake ODFS with a test file.
SetUpTest();
const TaskDescriptor open_in_office_task = CreateWebDriveOfficeTask();
std::vector<storage::FileSystemURL> file_urls{one_drive_test_file_url_};
// This boolean only becomes `True` if the fake provided OneDrive file system
// observes the test file being opened.
file_opened_ = false;
// Fails as system is offline and thus will open dialog.
ExecuteFileTask(
profile(), open_in_office_task, file_urls,
base::BindOnce(
[](extensions::api::file_manager_private::TaskResult result,
std::string error_message) {}));
ASSERT_FALSE(file_opened_);
SetConnectionOnline();
// Run dialog callback, simulate user choosing to "try-again". Will succeed
// because system is online.
OnDialogChoiceReceived(profile(), CreateOpenInOfficeTask(), file_urls,
ash::office_fallback::kDialogChoiceTryAgain);
ASSERT_TRUE(file_opened_);
}
// Test to check that the test file fails to open when the system is offline and
// does not open from a "cancel" dialog choice even when the systems comes
// online.
IN_PROC_BROWSER_TEST_F(OfficeFallbackOneDriveTest, OneDriveCancel) {
// Creates a fake ODFS with a test file.
SetUpTest();
const TaskDescriptor open_in_office_task = CreateWebDriveOfficeTask();
std::vector<storage::FileSystemURL> file_urls{one_drive_test_file_url_};
// This boolean only becomes `True` if the fake provided OneDrive file system
// observes the test file being opened.
file_opened_ = false;
// Fails as system is offline and thus will open dialog.
ExecuteFileTask(
profile(), open_in_office_task, file_urls,
base::BindOnce(
[](extensions::api::file_manager_private::TaskResult result,
std::string error_message) {}));
ASSERT_FALSE(file_opened_);
SetConnectionOnline();
// Run dialog callback, simulate user choosing to "cancel". The file will not
// open.
OnDialogChoiceReceived(profile(), CreateOpenInOfficeTask(), file_urls,
ash::office_fallback::kDialogChoiceCancel);
ASSERT_FALSE(file_opened_);
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
// TODO(cassycc) figure out how to run all browser test types for
// Office Fallback tests?
INSTANTIATE_SYSTEM_WEB_APP_MANAGER_TEST_SUITE_ALL_PROFILE_TYPES_P(
FileTasksBrowserTest);
} // namespace file_tasks
} // namespace file_manager