// 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/file_manager/file_tasks.h"
#include <cstring>
#include <memory>
#include <string_view>
#include <unordered_map>
#include "ash/webui/file_manager/url_constants.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/path_service.h"
#include "base/ranges/algorithm.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/threading/thread_restrictions.h"
#include "build/branding_buildflags.h"
#include "chrome/browser/apps/app_service/app_launch_params.h"
#include "chrome/browser/apps/app_service/app_registry_cache_waiter.h"
#include "chrome/browser/apps/app_service/app_service_proxy.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/publishers/app_publisher.h"
#include "chrome/browser/ash/arc/fileapi/arc_documents_provider_util.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/crosapi/test_controller_ash.h"
#include "chrome/browser/ash/drive/file_system_util.h"
#include "chrome/browser/ash/extensions/file_manager/event_router_factory.h"
#include "chrome/browser/ash/file_manager/app_id.h"
#include "chrome/browser/ash/file_manager/file_manager_browsertest_base.h"
#include "chrome/browser/ash/file_manager/file_manager_test_util.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/office_file_tasks.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/url_util.h"
#include "chrome/browser/ash/file_manager/volume.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/provided_file_system_info.h"
#include "chrome/browser/ash/file_system_provider/provider_interface.h"
#include "chrome/browser/ash/login/test/guest_session_mixin.h"
#include "chrome/browser/ash/login/test/logged_in_user_mixin.h"
#include "chrome/browser/ash/policy/dlp/dlp_files_controller_ash.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_rules_manager.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/chromeos/upload_office_to_cloud/upload_office_to_cloud.h"
#include "chrome/browser/extensions/extension_keeplist_chromeos.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/notifications/notification_display_service_factory.h"
#include "chrome/browser/policy/profile_policy_connector.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/ui/web_applications/web_app_launch_process.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_open_metrics.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/hats_office_trigger.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/extensions/extension_constants.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/chromeos/ash_browser_test_starter.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/mixin_based_in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/ash/components/drivefs/mojom/drivefs.mojom.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/crosapi/mojom/test_controller.mojom-test-utils.h"
#include "chromeos/crosapi/mojom/test_controller.mojom.h"
#include "components/services/app_service/public/cpp/app_instance_waiter.h"
#include "components/services/app_service/public/cpp/app_types.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 "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/features.h"
#include "ui/message_center/public/cpp/notification.h"
namespace file_manager::file_tasks {
namespace {
using blink::features::kFileHandlingAPI;
using drive::DriveIntegrationService;
using drive::util::ConnectionStatus;
using drive::util::SetDriveConnectionStatusForTesting;
using extensions::api::file_manager_private::TaskResult;
using network::TestNetworkConnectionTracker;
using web_app::kMediaAppId;
const char* blockedUrl = "https://blocked.com";
// 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.
// A source_url can be set when testing DLP restrictions and is also used to
// determine whether fetched tasks should be blocked. If null, the task should
// never be blocked.
struct Expectation {
const char* file_extensions;
const char* app_id;
const char* mime_type = nullptr;
const char* dlp_source_url = 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();
}
// Populates |entries|, |file_urls|, and |dlp_source_urls| based on |test|.
void ConvertExpectation(const Expectation& test,
std::vector<extensions::EntryInfo>& entries,
std::vector<GURL>& file_urls,
std::vector<std::string>& dlp_source_urls) {
const base::FilePath prefix = base::FilePath().AppendASCII("file");
std::vector<std::string_view> all_extensions = base::SplitStringPiece(
test.file_extensions, "/", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
for (std::string_view extension : all_extensions) {
base::FilePath path = prefix.AddExtension(extension);
std::string mime_type;
base::ScopedAllowBlockingForTesting allow_blocking;
net::GetMimeTypeFromFile(path, &mime_type);
if (test.mime_type != nullptr) {
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);
dlp_source_urls.push_back(test.dlp_source_url ? test.dlp_source_url : "");
}
}
// The Office Fallback Dialog only exists when the is_chrome_branded GN flag set
// because otherwise QuickOffice is not installed.
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
content::WebContents* GetWebContentsFromOfficeFallbackAndWaitForDialog() {
ash::SystemWebDialogDelegate* dialog =
ash::SystemWebDialogDelegate::FindInstance(
chrome::kChromeUIOfficeFallbackURL);
EXPECT_TRUE(dialog);
content::WebUI* webui = dialog->GetWebUIForTest();
EXPECT_TRUE(webui);
content::WebContents* web_contents = webui->GetWebContents();
EXPECT_TRUE(web_contents);
// Wait until the DOM element actually exists at office-fallback.
EXPECT_TRUE(base::test::RunUntil([&] {
return content::EvalJs(web_contents,
"!!document.querySelector('office-fallback')")
.ExtractBool();
}));
return web_contents;
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
// Auxiliary class for abstracting over differences between the crosapi and
// non-crosapi test variants.
class TestController {
public:
virtual ~TestController() = default;
virtual void SetUpInProcessBrowserTestFixture() = 0;
virtual void SetUpOnMainThread(InProcessBrowserTest* test_class_obj) {
profile_ = test_class_obj->browser()->profile();
}
virtual void TearDownOnMainThread() { profile_ = nullptr; }
virtual std::string InstallExtension(const char* path) = 0;
virtual void RemoveComponentExtension(const std::string& extension_id) = 0;
virtual std::string ExecuteFileTaskAndWaitForDomMessage(
const TaskDescriptor& task,
const std::vector<storage::FileSystemURL>& files) = 0;
protected:
TestController() = default;
Profile* profile() { return profile_; }
private:
raw_ptr<Profile> profile_ = nullptr;
};
// Class for letting the (Ash) test observe Lacros DOM messages.
class DomMessageObserverAsh : public crosapi::mojom::DomMessageObserver {
public:
explicit DomMessageObserverAsh(
base::test::TestFuture<std::string>* message_future)
: message_future_(message_future) {}
mojo::PendingRemote<crosapi::mojom::DomMessageObserver> Bind() {
return receiver_.BindNewPipeAndPassRemote();
}
private:
void OnMessage(const std::string& message) override {
message_future_->SetValue(message);
}
mojo::Receiver<crosapi::mojom::DomMessageObserver> receiver_{this};
raw_ptr<base::test::TestFuture<std::string>> message_future_;
};
// For testing with crosapi enabled. Makes use of
// StandaloneBrowserTestController to talk to Lacros.
class TestControllerLacros : public TestController {
public:
TestControllerLacros() = default;
~TestControllerLacros() override = default;
void SetUpInProcessBrowserTestFixture() override {
if (lacros_starter_.HasLacrosArgument()) {
ASSERT_TRUE(lacros_starter_.PrepareEnvironmentForLacros());
}
}
void SetUpOnMainThread(InProcessBrowserTest* test_class_obj) override {
TestController::SetUpOnMainThread(test_class_obj);
if (!lacros_starter_.HasLacrosArgument()) {
GTEST_SKIP() << "This test needs to run together with Lacros but the "
"--lacros-chrome-path switch is missing.";
}
lacros_starter_.StartLacros(test_class_obj);
// Wait until StandaloneBrowserTestController binds with TestControllerAsh.
CHECK(crosapi::TestControllerAsh::Get());
base::test::TestFuture<void> waiter;
crosapi::TestControllerAsh::Get()
->on_standalone_browser_test_controller_bound()
.Post(FROM_HERE, waiter.GetCallback());
ASSERT_TRUE(waiter.Wait())
<< "Could not bind StandaloneBrowserTestController - make sure that "
"the --lacros-chrome-path value points to a test_lacros_chrome "
"binary (the last component should not be a directory).";
lacros_waiter_.emplace(crosapi::TestControllerAsh::Get()
->GetStandaloneBrowserTestController());
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
// QuickOffice loads from rootfs at /usr/share/chromeos-assets/quickoffce
// which does not exist on bots for tests, so load test version.
base::FilePath data_dir;
CHECK(base::PathService::Get(chrome::DIR_TEST_DATA, &data_dir));
lacros_waiter_->InstallComponentExtension(
data_dir.Append("chromeos/file_manager/quickoffice").value(),
extension_misc::kQuickOfficeComponentExtensionId);
#endif
}
std::string InstallExtension(const char* path) override {
base::FilePath data_dir;
CHECK(base::PathService::Get(chrome::DIR_TEST_DATA, &data_dir));
return lacros_waiter_->InstallUnpackedExtension(
data_dir.Append(path).value());
}
void RemoveComponentExtension(const std::string& extension_id) override {
lacros_waiter_->RemoveComponentExtension(extension_id);
}
std::string ExecuteFileTaskAndWaitForDomMessage(
const TaskDescriptor& task,
const std::vector<storage::FileSystemURL>& files) override {
// Extension background page may not have finished loading yet, but the
// FileTask machinery will wait for that.
base::test::TestFuture<std::string> message;
DomMessageObserverAsh observer(&message);
lacros_waiter_->ObserveDomMessages(observer.Bind());
ExecuteFileTask(profile(), task, files, base::DoNothing());
return message.Get();
}
private:
::test::AshBrowserTestStarter lacros_starter_;
std::optional<crosapi::mojom::StandaloneBrowserTestControllerAsyncWaiter>
lacros_waiter_;
};
// For testing with crosapi disabled.
class TestControllerAsh : public TestController {
public:
TestControllerAsh() = default;
~TestControllerAsh() override = default;
void SetUpInProcessBrowserTestFixture() override {}
void SetUpOnMainThread(InProcessBrowserTest* test_class_obj) override {
TestController::SetUpOnMainThread(test_class_obj);
test::AddDefaultComponentExtensionsOnMainThread(profile());
}
std::string InstallExtension(const char* path) override {
return test::InstallTestingChromeApp(profile(), path)->id();
}
void RemoveComponentExtension(const std::string& extension_id) override {
extensions::ExtensionSystem::Get(profile())
->extension_service()
->RemoveComponentExtension(extension_id);
}
std::string ExecuteFileTaskAndWaitForDomMessage(
const TaskDescriptor& task,
const std::vector<storage::FileSystemURL>& files) override {
content::DOMMessageQueue message_queue;
ExecuteFileTask(profile(), task, files, base::DoNothing());
std::string message;
CHECK(message_queue.WaitForMessage(&message));
return message;
}
};
// Helper to exit a RunLoop when the given WebContents is destroyed.
class WebContentsDestroyedWaiter : public content::WebContentsObserver {
public:
WebContentsDestroyedWaiter(
content::WebContents* contents,
const base::RepeatingClosure& on_web_contents_destroyed)
: content::WebContentsObserver(contents),
on_web_contents_destroyed_(on_web_contents_destroyed) {}
~WebContentsDestroyedWaiter() override = default;
// Override from WebContentsObserver.
void WebContentsDestroyed() override { on_web_contents_destroyed_.Run(); }
base::RepeatingClosure on_web_contents_destroyed_;
};
class FileTasksBrowserTest : public TestProfileTypeMixin<InProcessBrowserTest> {
public:
FileTasksBrowserTest() : TestProfileTypeMixin<InProcessBrowserTest>() {
if (GetParam().crosapi_state == TestProfileParam::CrosapiParam::kEnabled) {
test_controller_ = std::make_unique<TestControllerLacros>();
} else {
test_controller_ = std::make_unique<TestControllerAsh>();
}
}
void SetUpInProcessBrowserTestFixture() override {
TestProfileTypeMixin<
InProcessBrowserTest>::SetUpInProcessBrowserTestFixture();
test_controller()->SetUpInProcessBrowserTestFixture();
}
void SetUpOnMainThread() override {
TestProfileTypeMixin<InProcessBrowserTest>::SetUpOnMainThread();
ash::SystemWebAppManager::GetForTest(browser()->profile())
->InstallSystemAppsForTesting();
test_controller()->SetUpOnMainThread(this);
}
void TearDownOnMainThread() override {
test_controller()->TearDownOnMainThread();
TestProfileTypeMixin<InProcessBrowserTest>::TearDownOnMainThread();
}
// 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();
for (const Expectation& test : expectations) {
std::vector<extensions::EntryInfo> entries;
std::vector<GURL> file_urls;
std::vector<std::string> dlp_source_urls;
ConvertExpectation(test, entries, file_urls, dlp_source_urls);
// task_verifier callback is invoked synchronously from
// FindAllTypesOfTasks.
FindAllTypesOfTasks(browser()->profile(), entries, file_urls,
dlp_source_urls,
base::BindOnce(&VerifyTasks, &remaining, test));
}
EXPECT_EQ(0, remaining);
}
std::string InstallTiffHandlerChromeApp() {
return test_controller()->InstallExtension(
"extensions/api_test/file_browser/app_file_handler");
}
protected:
TestController* test_controller() { return test_controller_.get(); }
private:
base::test::ScopedFeatureList feature_list_{kFileHandlingAPI};
std::unique_ptr<TestController> test_controller_;
};
} // 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", false},
{"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);
base::ScopedAllowBlockingForTesting allow_blocking;
if (test.has_mime) {
EXPECT_TRUE(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, "image/tiff"},
{"orf", kMediaAppId},
{"raf", kMediaAppId},
{"rw2", kMediaAppId},
{"NRW", kMediaAppId, "image/tiff"}, // 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.
// NOTE: For the Crosapi variant, Lacros must be a branded build as well,
// otherwise the test will fail.
// Disabled due to crbug.com/347884251.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, DISABLED_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;
}
std::string extension_id = InstallTiffHandlerChromeApp();
TestExpectationsAgainstDefaultTasks({{"tiff", kMediaAppId}});
UpdateDefaultTask(
browser()->profile(),
TaskDescriptor(extension_id, StringToTaskType("app"), "tiffAction"),
{"tiff"}, {"image/tiff"});
if (profile_type() == TestProfileType::kIncognito &&
GetParam().crosapi_state == TestProfileParam::CrosapiParam::kDisabled) {
// If installed 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,
base::BindOnce(base::IgnoreResult(&TestController::InstallExtension),
base::Unretained(test_controller())));
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 = web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
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.
webapps::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_unsafe()
.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::WebAppLaunchProcess::SetOpenApplicationCallbackForTesting(
base::BindLambdaForTesting(
[&run_loop](apps::AppLaunchParams params) {
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();
}));
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;
}
std::string extension_id = InstallTiffHandlerChromeApp();
Profile* const profile = browser()->profile();
std::vector<storage::FileSystemURL> files =
test::CopyTestFilesIntoMyFiles(profile, {"test_small.tiff"});
TaskDescriptor task_descriptor(extension_id, TASK_TYPE_FILE_HANDLER,
"tiffAction");
std::string message = test_controller()->ExecuteFileTaskAndWaitForDomMessage(
task_descriptor, files);
ASSERT_EQ("\"Received tiffAction with: test_small.tiff\"", message);
}
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);
}
const FileSystemURL CreateOfficeFileSourceURL(Profile* profile,
std::string name = "text.docx") {
base::FilePath file =
util::GetMyFilesFolderForProfile(profile).AppendASCII(name);
return ash::cloud_upload::FilePathToFileSystemURL(
profile, file_manager::util::GetFileManagerFileSystemContext(profile),
file);
}
// These tests only run with the is_chrome_branded GN flag set because otherwise
// QuickOffice is not installed.
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
// Test that the Fallback dialog can be shown when Quick Office is installed.
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, FallbackSucceedsWithQuickOffice) {
if (profile_type() == TestProfileType::kIncognito) {
GTEST_SKIP()
<< "There is no AppServiceProxy for incognito profiles as they are "
"ephemeral and have no apps persisted inside them.";
}
storage::FileSystemURL test_url;
Profile* const profile = browser()->profile();
// GetUserFallbackChoice() returns `True` because the Fallback dialog can be
// shown.
const TaskDescriptor task = CreateWebDriveOfficeTask();
std::vector<FileSystemURL> file_url{test_url};
ash::office_fallback::FallbackReason fallback_reason =
ash::office_fallback::FallbackReason::kOffline;
ash::office_fallback::DialogChoiceCallback callback = base::BindOnce(
&OnDialogChoiceReceived, profile, task, file_url, fallback_reason,
std::make_unique<ash::cloud_upload::CloudOpenMetrics>(
ash::cloud_upload::CloudProvider::kOneDrive, /*file_count=*/1));
ASSERT_TRUE(GetUserFallbackChoice(
profile, task, file_url, ash::office_fallback::FallbackReason::kOffline,
std::move(callback)));
}
IN_PROC_BROWSER_TEST_P(FileTasksBrowserTest, FallbackFailsNoQuickOffice) {
if (profile_type() == TestProfileType::kIncognito) {
GTEST_SKIP()
<< "There is no AppServiceProxy, which is required to check "
"QuickOffice is installed, for incognito profiles as they are "
"ephemeral and have no apps persisted inside them.";
}
// Uninstall QuickOffice.
test_controller()->RemoveComponentExtension(
extension_misc::kQuickOfficeComponentExtensionId);
// GetUserFallbackChoice() returns `False` because QuickOffice is not
// installed.
storage::FileSystemURL test_url;
Profile* const profile = browser()->profile();
const TaskDescriptor task = CreateWebDriveOfficeTask();
std::vector<FileSystemURL> file_url{test_url};
ash::office_fallback::FallbackReason fallback_reason =
ash::office_fallback::FallbackReason::kOffline;
ash::office_fallback::DialogChoiceCallback callback = base::BindOnce(
&OnDialogChoiceReceived, profile, task, file_url, fallback_reason,
std::make_unique<ash::cloud_upload::CloudOpenMetrics>(
ash::cloud_upload::CloudProvider::kOneDrive, /*file_count=*/1));
ASSERT_FALSE(GetUserFallbackChoice(profile, task, file_url, fallback_reason,
std::move(callback)));
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
// Tests that apply DLP policies before fetching tasks and verify expectations
// on the blocked status.
class FileTasksPolicyBrowserTest : public FileTasksBrowserTest {
public:
// Tests that fetched tasks are marked as blocked by DLP, if expected.
void TestExpectationsAgainstDlp(
const std::vector<Expectation>& expectations) {
for (const Expectation& test : expectations) {
std::vector<extensions::EntryInfo> entries;
std::vector<GURL> file_urls;
std::vector<std::string> dlp_source_urls;
ConvertExpectation(test, entries, file_urls, dlp_source_urls);
base::test::TestFuture<std::unique_ptr<ResultingTasks>> tasks_future;
FindAllTypesOfTasks(browser()->profile(), entries, file_urls,
dlp_source_urls, tasks_future.GetCallback());
ASSERT_TRUE(tasks_future.Get()) << test.file_extensions;
ResultingTasks& resulting_tasks = *tasks_future.Get();
// Verifies that all tasks are either blocked or not by DLP, according to
// |test|.
bool expect_dlp_blocked =
test.dlp_source_url && strcmp(test.dlp_source_url, blockedUrl) == 0;
EXPECT_EQ(expect_dlp_blocked,
base::ranges::all_of(resulting_tasks.tasks,
&FullTaskDescriptor::is_dlp_blocked));
}
}
std::unique_ptr<KeyedService> SetDlpRulesManager(
content::BrowserContext* context) {
auto dlp_rules_manager =
std::make_unique<testing::NiceMock<policy::MockDlpRulesManager>>(
Profile::FromBrowserContext(context));
rules_manager_ = dlp_rules_manager.get();
return dlp_rules_manager;
}
protected:
raw_ptr<policy::MockDlpRulesManager, DanglingUntriaged> rules_manager_ =
nullptr;
};
IN_PROC_BROWSER_TEST_P(FileTasksPolicyBrowserTest, TasksMarkedAsBlocked) {
if (profile_type() != TestProfileType::kRegular) {
// Early return: DLP is only supported for regular profiles.
return;
}
Profile* profile = browser()->profile();
policy::DlpRulesManagerFactory::GetInstance()->SetTestingFactory(
profile,
base::BindRepeating(&FileTasksPolicyBrowserTest::SetDlpRulesManager,
base::Unretained(this)));
ASSERT_TRUE(policy::DlpRulesManagerFactory::GetForPrimaryProfile());
auto files_controller =
std::make_unique<policy::DlpFilesControllerAsh>(*rules_manager_, profile);
ON_CALL(*rules_manager_, GetDlpFilesController)
.WillByDefault(testing::Return(files_controller.get()));
ON_CALL(*rules_manager_, IsFilesPolicyEnabled)
.WillByDefault(testing::Return(true));
EXPECT_CALL(*rules_manager_, IsRestrictedDestination)
.Times(testing::AtLeast(1))
.WillRepeatedly(testing::Return(policy::DlpRulesManager::Level::kAllow));
EXPECT_CALL(*rules_manager_,
IsRestrictedDestination(GURL(blockedUrl), testing::_, testing::_,
testing::_, testing::_, testing::_))
.Times(testing::AtLeast(1))
.WillRepeatedly(testing::Return(policy::DlpRulesManager::Level::kBlock));
std::vector<Expectation> expectations = {
{"jpg/gif", kMediaAppId, nullptr, "https://example.com"},
{"jpg/mp4", kMediaAppId, nullptr, "https://blocked.com"},
};
TestExpectationsAgainstDlp(expectations);
}
// |InProcessBrowserTest| which allows a fake user to login. Login a non-managed
// to ensure |IsEligibleAndEnabledUploadOfficeToCloud| returns the result of
// |IsUploadOfficeToCloudEnabled|.
class TestAccountBrowserTest : public MixinBasedInProcessBrowserTest {
public:
explicit TestAccountBrowserTest(TestAccountType test_account_type) {
ash::LoggedInUserMixin::LogInType log_in_type =
LogInTypeFor(test_account_type);
std::optional<AccountId> account_id = AccountIdFor(test_account_type);
logged_in_user_mixin_ = std::make_unique<ash::LoggedInUserMixin>(
&mixin_host_, /*test_base=*/this, embedded_test_server(), log_in_type,
/*include_initial_user=*/true, account_id);
}
// Launch Files app and wait for it to open.
void LaunchFilesAppAndWait() {
GURL files_swa_url = util::GetFileManagerMainPageUrlWithParams(
ui::SelectFileDialog::SELECT_NONE, /*title=*/std::u16string(),
/*current_directory_url=*/{},
/*selection_url=*/GURL(),
/*target_name=*/{}, /*file_types=*/{},
/*file_type_index=*/0,
/*search_query=*/{},
/*show_android_picker_apps=*/false,
/*volume_filter=*/{});
ash::SystemAppLaunchParams params;
params.url = files_swa_url;
ash::LaunchSystemWebAppAsync(browser()->profile(),
ash::SystemWebAppType::FILE_MANAGER, params);
ui_test_utils::WaitForBrowserToOpen();
}
protected:
void SetUpOnMainThread() override {
MixinBasedInProcessBrowserTest::SetUpOnMainThread();
logged_in_user_mixin_->LogInUser();
// Needed to launch Files app as the dialog's modal parent.
ash::SystemWebAppManager::GetForTest(browser()->profile())
->InstallSystemAppsForTesting();
}
private:
std::unique_ptr<ash::LoggedInUserMixin> logged_in_user_mixin_;
};
class NonManagedAccount : public TestAccountBrowserTest {
public:
NonManagedAccount() : TestAccountBrowserTest(kNonManaged) {
feature_list_.InitAndEnableFeature(
chromeos::features::kUploadOfficeToCloud);
}
void SetUpOnMainThread() override {
TestAccountBrowserTest::SetUpOnMainThread();
app_service_test_.SetUp(browser()->profile());
auto fake_provider =
ash::file_system_provider::FakeExtensionProvider::Create(
extension_misc::kODFSExtensionId);
const auto kProviderId = fake_provider->GetId();
auto* service =
ash::file_system_provider::Service::Get(browser()->profile());
service->RegisterProvider(std::move(fake_provider));
}
apps::AppServiceProxy* app_service_proxy() {
apps::AppServiceProxy* app_service_proxy =
apps::AppServiceProxyFactory::GetForProfile(browser()->profile());
CHECK(app_service_proxy);
return app_service_proxy;
}
private:
base::test::ScopedFeatureList feature_list_;
apps::AppServiceTest app_service_test_;
};
// Tests that IsEligibleAndEnabledUploadOfficeToCloud() returns true when a
// non-managed user is logged in and |kUploadOfficeToCloud| is enabled.
IN_PROC_BROWSER_TEST_F(NonManagedAccount,
IsEligibleAndEnabledUploadOfficeToCloud) {
ASSERT_TRUE(
chromeos::IsEligibleAndEnabledUploadOfficeToCloud(browser()->profile()));
}
class WithEnterpriseFlag : public TestAccountBrowserTest {
public:
explicit WithEnterpriseFlag(bool is_managed)
: TestAccountBrowserTest(is_managed ? kEnterprise : kNonManaged) {
feature_list_.InitWithFeatures(
{chromeos::features::kUploadOfficeToCloud,
chromeos::features::kUploadOfficeToCloudForEnterprise},
{});
}
private:
base::test::ScopedFeatureList feature_list_;
};
class NonManagedAccountWithEnterpriseFlag : public WithEnterpriseFlag {
public:
NonManagedAccountWithEnterpriseFlag()
: WithEnterpriseFlag(/*is_managed=*/false) {}
};
// Tests that IsEligibleAndEnabledUploadOfficeToCloud() returns true when a
// non-managed user is logged in and both |kUploadOfficeToCloud| and
// |kUploadOfficeToCloudForEnterprise| are enabled.
IN_PROC_BROWSER_TEST_F(NonManagedAccountWithEnterpriseFlag,
IsEligibleAndEnabledUploadOfficeToCloud) {
ASSERT_TRUE(
chromeos::IsEligibleAndEnabledUploadOfficeToCloud(browser()->profile()));
}
class WithEnterpriseFlagAndPrefs
: public WithEnterpriseFlag,
public testing::WithParamInterface<
std::tuple<std::string_view /* google_workspace_cloud_upload */,
std::string_view /* microsoft_office_cloud_upload */,
bool /* odfs_extension_installed */,
bool /* is_managed */>> {
public:
WithEnterpriseFlagAndPrefs()
: WithEnterpriseFlag(/*is_managed=*/std::get<3>(GetParam())) {}
void SetUpOnMainThread() override {
TestAccountBrowserTest::SetUpOnMainThread();
app_service_test_.SetUp(browser()->profile());
if (std::get<2>(GetParam())) {
auto fake_provider =
ash::file_system_provider::FakeExtensionProvider::Create(
extension_misc::kODFSExtensionId);
const auto kProviderId = fake_provider->GetId();
auto* service =
ash::file_system_provider::Service::Get(browser()->profile());
service->RegisterProvider(std::move(fake_provider));
}
}
private:
apps::AppServiceTest app_service_test_;
};
// Tests that GoogleWorkspace and Microsoft365 tasks are only present in the
// list of file tasks if the corresponding prefs allow it.
IN_PROC_BROWSER_TEST_P(WithEnterpriseFlagAndPrefs,
GoogleWorkspaceAndMicrosoft365Tasks) {
auto* profile = browser()->profile();
auto [google_workspace_cloud_upload, microsoft_office_cloud_upload,
odfs_extension_installed, is_managed] = GetParam();
profile->GetPrefs()->SetString(prefs::kGoogleWorkspaceCloudUpload,
google_workspace_cloud_upload);
profile->GetPrefs()->SetString(prefs::kMicrosoftOfficeCloudUpload,
microsoft_office_cloud_upload);
for (const auto& extension_group :
{WordGroupExtensions(), ExcelGroupExtensions(),
PowerPointGroupExtensions()}) {
for (const auto& extension : extension_group) {
base::FilePath test_file_path =
web_app::CreateTestFileWithExtension(extension);
std::vector<FullTaskDescriptor> tasks =
file_manager::test::GetTasksForFile(profile, test_file_path);
const size_t google_workspace_task_count = base::ranges::count_if(
tasks, &IsWebDriveOfficeTask, &FullTaskDescriptor::task_descriptor);
EXPECT_EQ(
google_workspace_task_count,
chromeos::cloud_upload::IsGoogleWorkspaceCloudUploadAllowed(profile)
? 1U
: 0U);
const size_t microsoft_office_task_count = base::ranges::count_if(
tasks, &IsOpenInOfficeTask, &FullTaskDescriptor::task_descriptor);
EXPECT_EQ(microsoft_office_task_count,
chromeos::cloud_upload::IsMicrosoftOfficeCloudUploadAllowed(
profile) &&
(!is_managed || odfs_extension_installed)
? 1U
: 0U);
}
}
}
INSTANTIATE_TEST_SUITE_P(
/**/,
WithEnterpriseFlagAndPrefs,
testing::Combine(
testing::Values(chromeos::cloud_upload::kCloudUploadPolicyDisallowed,
chromeos::cloud_upload::kCloudUploadPolicyAllowed,
chromeos::cloud_upload::kCloudUploadPolicyAutomated),
testing::Values(chromeos::cloud_upload::kCloudUploadPolicyDisallowed,
chromeos::cloud_upload::kCloudUploadPolicyAllowed,
chromeos::cloud_upload::kCloudUploadPolicyAutomated),
testing::Bool(),
testing::Bool()));
// Test that the office PWA file handler is hidden from the available file
// handlers when opening an office file and the |kUploadOfficeToCloud| flag is
// enabled.
IN_PROC_BROWSER_TEST_F(NonManagedAccount, OfficePwaHandlerHidden) {
struct FakeOfficeFileType {
std::string file_extension;
std::string mime_type;
};
std::vector<FakeOfficeFileType> fake_office_file_types = {
{"ppt", "application/vnd.ms-powerpoint"},
{"pptx",
"application/"
"vnd.openxmlformats-officedocument.presentationml.presentation"},
{"xls", "application/vnd.ms-excel"},
{"xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
{"doc", "application/msword"},
{"docx",
"application/"
"vnd.openxmlformats-officedocument.wordprocessingml.document"}};
for (FakeOfficeFileType& fake_office_file_type : fake_office_file_types) {
file_manager::test::AddFakeWebApp(web_app::kMicrosoft365AppId,
fake_office_file_type.mime_type,
fake_office_file_type.file_extension,
"something", true, app_service_proxy());
base::FilePath test_file_path = web_app::CreateTestFileWithExtension(
fake_office_file_type.file_extension);
std::vector<file_manager::file_tasks::FullTaskDescriptor> tasks =
file_manager::test::GetTasksForFile(browser()->profile(),
test_file_path);
for (FullTaskDescriptor& task : tasks) {
EXPECT_NE(web_app::kMicrosoft365AppId, task.task_descriptor.app_id)
<< " for extension: " << fake_office_file_type.file_extension;
}
}
}
class EnterpriseAccount : public TestAccountBrowserTest {
public:
EnterpriseAccount() : TestAccountBrowserTest(kEnterprise) {
feature_list_.InitAndEnableFeature(
chromeos::features::kUploadOfficeToCloud);
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Tests that IsEligibleAndEnabledUploadOfficeToCloud() returns true when an
// enterprise user is logged in and |kUploadOfficeToCloud| is enabled.
IN_PROC_BROWSER_TEST_F(EnterpriseAccount,
IsEligibleAndEnabledUploadOfficeToCloud) {
ASSERT_TRUE(
chromeos::IsEligibleAndEnabledUploadOfficeToCloud(browser()->profile()));
}
class EnterpriseAccountWithEnterpriseFlag : public TestAccountBrowserTest {
public:
EnterpriseAccountWithEnterpriseFlag() : TestAccountBrowserTest(kEnterprise) {
feature_list_.InitWithFeatures(
{chromeos::features::kUploadOfficeToCloud,
chromeos::features::kUploadOfficeToCloudForEnterprise},
{});
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Tests that IsEligibleAndEnabledUploadOfficeToCloud() returns true when an
// enterprise user is logged in and both |kUploadOfficeToCloud| and
// |kUploadOfficeToCloudForEnterprise| are enabled.
IN_PROC_BROWSER_TEST_F(EnterpriseAccountWithEnterpriseFlag,
IsEligibleAndEnabledUploadOfficeToCloud) {
ASSERT_TRUE(
chromeos::IsEligibleAndEnabledUploadOfficeToCloud(browser()->profile()));
}
class ChildAccount : public TestAccountBrowserTest {
public:
ChildAccount() : TestAccountBrowserTest(kChild) {
feature_list_.InitAndEnableFeature(
chromeos::features::kUploadOfficeToCloud);
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Tests that IsEligibleAndEnabledUploadOfficeToCloud() returns false when a
// child user is logged in and |kUploadOfficeToCloud| is enabled.
IN_PROC_BROWSER_TEST_F(ChildAccount, IsEligibleAndEnabledUploadOfficeToCloud) {
ASSERT_FALSE(
chromeos::IsEligibleAndEnabledUploadOfficeToCloud(browser()->profile()));
}
class ChildAccountWithEnterpriseFlag : public TestAccountBrowserTest {
public:
ChildAccountWithEnterpriseFlag() : TestAccountBrowserTest(kChild) {
feature_list_.InitWithFeatures(
{chromeos::features::kUploadOfficeToCloud,
chromeos::features::kUploadOfficeToCloudForEnterprise},
{});
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Tests that IsEligibleAndEnabledUploadOfficeToCloud() returns false when a
// child user is logged in and both |kUploadOfficeToCloud| and
// |kUploadOfficeToCloudForEnterprise| are enabled.
IN_PROC_BROWSER_TEST_F(ChildAccountWithEnterpriseFlag,
IsEligibleAndEnabledUploadOfficeToCloud) {
ASSERT_FALSE(
chromeos::IsEligibleAndEnabledUploadOfficeToCloud(browser()->profile()));
}
class NonManagedAccountNoFlag : public TestAccountBrowserTest {
public:
NonManagedAccountNoFlag() : TestAccountBrowserTest(kNonManaged) {
feature_list_.InitAndDisableFeature(
chromeos::features::kUploadOfficeToCloud);
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Tests that IsEligibleAndEnabledUploadOfficeToCloud() returns false when a
// non-managed user is logged in but |kUploadOfficeToCloud| is disabled.
IN_PROC_BROWSER_TEST_F(NonManagedAccountNoFlag,
IsEligibleAndEnabledUploadOfficeToCloud) {
ASSERT_FALSE(
chromeos::IsEligibleAndEnabledUploadOfficeToCloud(browser()->profile()));
}
// |InProcessBrowserTest| which allows a user to login to Guest mode.
class GuestMode : public MixinBasedInProcessBrowserTest {
public:
GuestMode() {
feature_list_.InitAndEnableFeature(
chromeos::features::kUploadOfficeToCloud);
}
protected:
ash::GuestSessionMixin guest_session_{&mixin_host_};
private:
base::test::ScopedFeatureList feature_list_;
};
// Tests that IsEligibleAndEnabledUploadOfficeToCloud() returns false when
// |kUploadOfficeToCloud| is enabled but the user is in Guest mode.
IN_PROC_BROWSER_TEST_F(GuestMode, IsEligibleAndEnabledUploadOfficeToCloud) {
ASSERT_FALSE(
chromeos::IsEligibleAndEnabledUploadOfficeToCloud(browser()->profile()));
}
// TODO(cassycc or petermarshall) share this class with other test files for
// testing with a fake DriveFs.
// Tests the cloud upload/open flow as well as the office fallback flow using a
// fake DriveFs.
class DriveTest : public TestAccountBrowserTest {
public:
DriveTest() : TestAccountBrowserTest(kNonManaged) {
feature_list_.InitAndEnableFeature(
chromeos::features::kUploadOfficeToCloud);
EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
drive_mount_point_ = temp_dir_.GetPath();
// Path of test file relative to the DriveFs mount point.
relative_test_file_path = base::FilePath("/").AppendASCII(test_file_name_);
cloud_open_metrics_ = std::make_unique<ash::cloud_upload::CloudOpenMetrics>(
ash::cloud_upload::CloudProvider::kGoogleDrive, /*file_count=*/1);
cloud_open_metrics_weak_ptr_ = cloud_open_metrics_->GetWeakPtr();
}
void SetUpInProcessBrowserTestFixture() override {
// Setup drive integration service.
create_drive_integration_service_ = base::BindRepeating(
&DriveTest::CreateDriveIntegrationService, base::Unretained(this));
service_factory_for_test_ = std::make_unique<
drive::DriveIntegrationServiceFactory::ScopedFactoryForTest>(
&create_drive_integration_service_);
}
void TearDown() override {
// If cloud_open_metrics_ was `std::move`d into the flow, it should have
// been destroyed by the end of the test.
if (!cloud_open_metrics_) {
ASSERT_TRUE(cloud_open_metrics_weak_ptr_.WasInvalidated());
}
TestAccountBrowserTest::TearDown();
storage::ExternalMountPoints::GetSystemInstance()->RevokeAllFileSystems();
}
DriveIntegrationService* CreateDriveIntegrationService(Profile* profile) {
base::ScopedAllowBlockingForTesting allow_blocking;
fake_drivefs_helpers_[profile] =
std::make_unique<test::FakeSimpleDriveFsHelper>(profile,
drive_mount_point_);
return new DriveIntegrationService(
profile, "", drive_mount_point_,
fake_drivefs_helpers_[profile]->CreateFakeDriveFsListenerFactory());
}
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()));
}
// Complete the set up of the fake DriveFs with a test file added.
void SetUpTest(
bool disable_set_up,
bool launch_files_app,
ConnectionStatus connection_status = ConnectionStatus::kConnected) {
// Install QuickOffice for the check in GetUserFallbackChoice() before
// the office fallback dialog can launched.
test::AddDefaultComponentExtensionsOnMainThread(profile());
// Create Drive root directory.
{
base::ScopedAllowBlockingForTesting allow_blocking;
EXPECT_TRUE(base::CreateDirectory(drive_mount_point_));
}
drivefs::FakeMetadata metadata;
metadata.path = relative_test_file_path;
metadata.alternate_url = alternate_url_;
// Add test file to the DriveFs.
fake_drivefs_helpers_[profile()]->fake_drivefs().SetMetadata(
std::move(metadata));
// Get URL for test file in the DriveFs.
FileSystemURL drive_test_file_url =
ash::cloud_upload::FilePathToFileSystemURL(
profile(),
file_manager::util::GetFileManagerFileSystemContext(profile()),
observed_absolute_drive_path());
file_urls_.push_back(drive_test_file_url);
if (disable_set_up) {
SetWordFileHandlerToFilesSWA(profile(), kActionIdWebDriveOfficeWord);
SetPowerPointFileHandlerToFilesSWA(profile(),
kActionIdWebDriveOfficePowerPoint);
SetExcelFileHandlerToFilesSWA(profile(), kActionIdWebDriveOfficeExcel);
}
if (launch_files_app) {
LaunchFilesAppAndWait();
}
SetDriveConnectionStatusForTesting(connection_status);
}
protected:
const std::string alternate_url_ =
"https://docs.google.com/document/d/smalldocxid?rtpof=true&usp=drive_fs";
const TaskDescriptor web_drive_office_task_ = CreateWebDriveOfficeTask();
std::vector<storage::FileSystemURL> file_urls_;
std::unique_ptr<ash::cloud_upload::CloudOpenMetrics> cloud_open_metrics_;
base::HistogramTester histogram_;
private:
base::ScopedTempDir temp_dir_;
base::FilePath drive_mount_point_;
const std::string test_file_name_ = "text.docx";
base::FilePath relative_test_file_path;
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<test::FakeSimpleDriveFsHelper>>
fake_drivefs_helpers_;
base::WeakPtr<ash::cloud_upload::CloudOpenMetrics>
cloud_open_metrics_weak_ptr_;
};
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
// Test to check that a DriveFS 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(DriveTest, OfficeFallbackTryAgain) {
// Add test file to fake DriveFS and disable the setup flow for office files
// because we want the office fallback dialog to run instead. Set no
// connection to Drive to trigger the fallback flow.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false,
ConnectionStatus::kNoNetwork);
// 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();
// Launches the office fallback dialog as the system is offline.
base::test::TestFuture<TaskResult, std::string> executed_future;
ExecuteFileTask(profile(), web_drive_office_task_, file_urls_,
executed_future.GetCallback());
ASSERT_EQ(executed_future.Get<0>(), TaskResult::kOpened);
// Wait for office fallback dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
SetDriveConnectionStatusForTesting(ConnectionStatus::kConnected);
// 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::FallbackReason::kOffline,
std::move(cloud_open_metrics_),
ash::office_fallback::kDialogChoiceTryAgain);
// Wait for file to open in web drive office.
navigation_observer_office.Wait();
histogram_.ExpectUniqueSample(
ash::cloud_upload::kDriveOpenSourceVolumeMetric,
ash::cloud_upload::OfficeFilesSourceVolume::kGoogleDrive, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kNumberOfFilesToOpenWithGoogleDriveMetric, 1, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kGoogleDrive, 1);
histogram_.ExpectUniqueSample(ash::cloud_upload::kDriveOpenSourceVolumeMetric,
VolumeType::VOLUME_TYPE_GOOGLE_DRIVE, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kNotRequired, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kGoogleDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kOpened, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kDriveErrorMetricName,
ash::cloud_upload::OfficeDriveOpenErrors::kSuccess, 1);
}
// Tests that when the fallback dialog closes unexpectedly, a Cancel TaskResult
// is logged.
IN_PROC_BROWSER_TEST_F(DriveTest, OfficeFallbackClosesUnexpectedly) {
// Add test file to fake DriveFS and disable the setup flow for office files
// because we want the office fallback dialog to run instead. Set no
// connection to Drive to trigger the fallback flow.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false,
ConnectionStatus::kNoNetwork);
// 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();
// Launches the office fallback dialog as the system is offline.
base::test::TestFuture<TaskResult, std::string> executed_future;
ExecuteFileTask(profile(), web_drive_office_task_, file_urls_,
executed_future.GetCallback());
ASSERT_EQ(executed_future.Get<0>(), TaskResult::kOpened);
// Wait for office fallback dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
// Get the web contents of chrome://office-fallback to be able to check that
// the dialog exists.
content::WebContents* web_contents =
GetWebContentsFromOfficeFallbackAndWaitForDialog();
// Close the dialog with no user response and wait for the dialog to close.
content::WebContentsDestroyedWatcher watcher(web_contents);
ash::SystemWebDialogDelegate* dialog =
ash::SystemWebDialogDelegate::FindInstance(
chrome::kChromeUIOfficeFallbackURL);
EXPECT_TRUE(dialog);
dialog->Close();
watcher.Wait();
histogram_.ExpectUniqueSample(
ash::cloud_upload::kNumberOfFilesToOpenWithGoogleDriveMetric, 1, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kGoogleDrive, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kGoogleDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kCancelledAtFallback, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kDriveErrorMetricName,
ash::cloud_upload::OfficeDriveOpenErrors::kOffline, 1);
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
// Test that the open-web-drive-office-word task opens an Office file located on
// Drive directly in the browser, if the Google Docs app isn't installed.
IN_PROC_BROWSER_TEST_F(DriveTest,
OpenDriveOfficeFileInBrowserWhenDocsAppNotInstalled) {
// Add test file to fake DriveFS.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false);
// 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();
ExecuteFileTask(profile(), web_drive_office_task_, file_urls_,
base::DoNothing());
// Wait for file to open in web drive office.
navigation_observer_office.Wait();
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kGoogleDrive, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kNumberOfFilesToOpenWithGoogleDriveMetric, 1, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kDriveOpenSourceVolumeMetric,
ash::cloud_upload::OfficeFilesSourceVolume::kGoogleDrive, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kNotRequired, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kGoogleDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kOpened, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kDriveErrorMetricName,
ash::cloud_upload::OfficeDriveOpenErrors::kSuccess, 1);
}
// Test that the open-web-drive-office-word task opens an Office file located on
// Drive in the Google Docs app, if the app is installed.
IN_PROC_BROWSER_TEST_F(DriveTest, OpenDriveOfficeFileInDocsAppWhenInstalled) {
// Add test file to fake DriveFS.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false);
// Install Google Docs PWA.
webapps::AppId docs_app_id = web_app::test::InstallDummyWebApp(
profile(), "Google Docs",
GURL("https://docs.google.com/document/?usp=installed_webapp"));
ASSERT_EQ(docs_app_id, web_app::kGoogleDocsAppId);
apps::AppReadinessWaiter(profile(), web_app::kGoogleDocsAppId).Await();
// Check that the apps opens by waiting for it to be not only started and
// running, but also active and visible.
auto expected_state = apps::InstanceState(apps::kStarted | apps::kRunning |
apps::kActive | apps::kVisible);
apps::AppInstanceWaiter waiter(
apps::AppServiceProxyFactory::GetForProfile(profile())
->InstanceRegistry(),
web_app::kGoogleDocsAppId, expected_state);
ExecuteFileTask(profile(), CreateWebDriveOfficeTask(), file_urls_,
base::DoNothing());
waiter.Await();
}
// Test that the setup flow for office files, that has never been run before,
// will be run when a Web Drive Office task tries to open an office file
// already in DriveFs.
IN_PROC_BROWSER_TEST_F(DriveTest, FileInDriveOpensSetUpDialog) {
// Add test file to fake DriveFs.
SetUpTest(/*disable_set_up=*/false, /*launch_files_app=*/true);
// Watch for dialog URL chrome://cloud-upload.
GURL expected_dialog_URL(chrome::kChromeUICloudUploadURL);
content::TestNavigationObserver navigation_observer_dialog(
expected_dialog_URL);
navigation_observer_dialog.StartWatchingNewWebContents();
// Triggers setup flow.
ExecuteFileTask(profile(), web_drive_office_task_, file_urls_,
base::DoNothing());
// Wait for setup flow dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
histogram_.ExpectUniqueSample(
ash::cloud_upload::kNumberOfFilesToOpenWithGoogleDriveMetric, 1, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kGoogleDrive, 1);
}
// Test that the setup flow for office files, that has never been run before,
// will be run when a Web Drive Office task tries to open an office file not
// already in DriveFs.
IN_PROC_BROWSER_TEST_F(DriveTest, FileNotInDriveOpensSetUpDialog) {
// Set up DriveFs.
SetUpTest(/*disable_set_up=*/false, /*launch_files_app=*/true);
// Create a Web Drive Office task to open the file from DriveFs. The file is
// not in the correct location for this task and would have to be moved to
// DriveFs.
FileSystemURL file_outside_drive = CreateOfficeFileSourceURL(profile());
std::vector<storage::FileSystemURL> file_urls{file_outside_drive};
// Watch for dialog URL chrome://cloud-upload.
GURL expected_dialog_URL(chrome::kChromeUICloudUploadURL);
content::TestNavigationObserver navigation_observer_dialog(
expected_dialog_URL);
navigation_observer_dialog.StartWatchingNewWebContents();
// Triggers setup flow.
ExecuteFileTask(profile(), web_drive_office_task_, file_urls,
base::DoNothing());
// Wait for setup flow dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
histogram_.ExpectUniqueSample(
ash::cloud_upload::kNumberOfFilesToOpenWithGoogleDriveMetric, 1, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kGoogleDrive, 1);
}
// Fake app service web app publisher to test when an app is launched.
class FakeWebAppPublisher : public apps::AppPublisher {
public:
struct LaunchEvent {
std::string app_id;
std::string intent_url;
};
explicit FakeWebAppPublisher(Profile* profile)
: AppPublisher(apps::AppServiceProxyFactory::GetForProfile(profile)) {
RegisterPublisher(apps::AppType::kWeb);
std::vector<apps::AppPtr> apps;
auto ms_web_app = std::make_unique<apps::App>(apps::AppType::kWeb,
web_app::kMicrosoft365AppId);
ms_web_app->readiness = apps::Readiness::kReady;
apps.push_back(std::move(ms_web_app));
Publish(std::move(apps), apps::AppType::kWeb,
/*should_notify_initialized=*/false);
}
void LoadIcon(const std::string& app_id,
const apps::IconKey& icon_key,
apps::IconType icon_type,
int32_t size_hint_in_dip,
bool allow_placeholder_icon,
apps::LoadIconCallback callback) override {
// Never called in these tests.
NOTREACHED_IN_MIGRATION();
}
void Launch(const std::string& app_id,
int32_t event_flags,
apps::LaunchSource launch_source,
apps::WindowInfoPtr window_info) override {
// Never called in these tests.
NOTREACHED_IN_MIGRATION();
}
void LaunchAppWithIntent(const std::string& app_id,
int32_t event_flags,
apps::IntentPtr intent,
apps::LaunchSource launch_source,
apps::WindowInfoPtr window_info,
apps::LaunchCallback callback) override {
launches_.push_back({
.app_id = app_id,
.intent_url = (intent && intent->url) ? intent->url->spec() : "",
});
}
void LaunchAppWithParams(apps::AppLaunchParams&& params,
apps::LaunchCallback callback) override {
// Never called in these tests.
NOTREACHED_IN_MIGRATION();
}
void ClearPastLaunches() { launches_.clear(); }
std::vector<LaunchEvent> GetLaunches() { return launches_; }
private:
std::vector<LaunchEvent> launches_;
};
// TODO(cassycc or petermarshall) share this class with other test files for
// testing with a fake ODFS.
// Tests the cloud upload/open flow as well as the office fallback flow using a
// fake ODFS.
class OneDriveTest : public TestAccountBrowserTest,
public NotificationDisplayService::Observer {
public:
OneDriveTest() : TestAccountBrowserTest(kNonManaged) {
feature_list_.InitAndEnableFeature(
chromeos::features::kUploadOfficeToCloud);
// Relative paths for files on ODFS and Android OneDrive.
relative_test_path_1_ = base::FilePath(test_docx_file_name_1_);
relative_test_path_2_ = base::FilePath(test_pptx_file_name_2_);
// The path in ODFS is the relative path with "/" prefixed.
test_path_within_odfs_1_ =
base::FilePath("/").Append(relative_test_path_1_);
test_path_within_odfs_2_ =
base::FilePath("/").Append(relative_test_path_2_);
cloud_open_metrics_ = std::make_unique<ash::cloud_upload::CloudOpenMetrics>(
ash::cloud_upload::CloudProvider::kOneDrive, /*file_count=*/1);
cloud_open_metrics_weak_ptr_ = cloud_open_metrics_->GetWeakPtr();
}
void TearDown() override {
// If cloud_open_metrics_ was `std::move`d into the flow, it should have
// been destroyed by the end of the test.
if (!cloud_open_metrics_) {
ASSERT_TRUE(cloud_open_metrics_weak_ptr_.WasInvalidated());
}
TestAccountBrowserTest::TearDown();
storage::ExternalMountPoints::GetSystemInstance()->RevokeAllFileSystems();
}
// Creates and mounts a fake ODFS with a test file. Installs QuickOffice for
// the check in GetUserFallbackChoice() before the dialog can launched.
void SetUpTest(bool disable_set_up,
bool launch_files_app,
bool connect_to_network = true) {
// Install QuickOffice for the check in GetUserFallbackChoice() before
// the office fallback dialog can launched.
test::AddDefaultComponentExtensionsOnMainThread(profile());
// Create a fake ODFS.
provided_file_system_ =
test::MountFakeProvidedFileSystemOneDrive(profile());
// Add test files to the fake ODFS.
provided_file_system_->AddEntry(
test_path_within_odfs_1_, false, test_docx_file_name_1_, 0,
base::Time::Now(),
"application/"
"vnd.openxmlformats-officedocument.wordprocessingml.document",
/*cloud_file_info=*/nullptr,
/*contents=*/"");
provided_file_system_->AddEntry(
test_path_within_odfs_2_, false, test_pptx_file_name_2_, 0,
base::Time::Now(),
"application/"
"vnd.openxmlformats-officedocument.presentationml.presentation",
/*cloud_file_info=*/nullptr,
/*contents=*/"");
// Get URLs of test files.
odfs_docx_test_file_url_1_ = ash::cloud_upload::FilePathToFileSystemURL(
profile(),
file_manager::util::GetFileManagerFileSystemContext(profile()),
AbsoluteOdfsTestPath1());
odfs_pptx_test_file_url_2_ = ash::cloud_upload::FilePathToFileSystemURL(
profile(),
file_manager::util::GetFileManagerFileSystemContext(profile()),
AbsoluteOdfsTestPath2());
// Set up for single-file open.
file_urls_.push_back(odfs_docx_test_file_url_1_);
if (disable_set_up) {
SetWordFileHandlerToFilesSWA(profile(), kActionIdOpenInOffice);
SetPowerPointFileHandlerToFilesSWA(profile(), kActionIdOpenInOffice);
SetExcelFileHandlerToFilesSWA(profile(), kActionIdOpenInOffice);
}
if (launch_files_app) {
// Do this before creating a FakeWebAppPublisher which would intercept
// Files app launching.
LaunchFilesAppAndWait();
}
web_app_publisher_ = std::make_unique<FakeWebAppPublisher>(profile());
SetNetworkConnected(connect_to_network);
}
Profile* profile() { return browser()->profile(); }
// A file path on ODFS which represents the fake file 1 in OneDrive. This file
// path can be used to open a file directly from ODFS using
// `OpenOrMoveFiles()`.
base::FilePath AbsoluteOdfsTestPath1() {
std::optional<ash::file_system_provider::ProvidedFileSystemInfo>
odfs_file_system_info = ash::cloud_upload::GetODFSInfo(profile());
EXPECT_TRUE(odfs_file_system_info.has_value());
return odfs_file_system_info->mount_path().Append(relative_test_path_1_);
}
// A file path on ODFS which represents the fake file 2 in OneDrive. This file
// path can be used to open a file directly from ODFS using
// `OpenOrMoveFiles()`.
base::FilePath AbsoluteOdfsTestPath2() {
std::optional<ash::file_system_provider::ProvidedFileSystemInfo>
odfs_file_system_info = ash::cloud_upload::GetODFSInfo(profile());
EXPECT_TRUE(odfs_file_system_info.has_value());
return odfs_file_system_info->mount_path().Append(relative_test_path_2_);
}
// Filesystem path for Android OneDrive documents provider to the directory
// that accesses the same OneDrive files as ODFS. The email is included in the
// Root Document Id. The file path to a file is constructed by appending the
// relative path (the part shared with ODFS). The file path on Android
// OneDrive can be converted to a ODFS file path (with no email attached)
// representing the same fake file in OneDrive. This file path can be used to
// open a file indirectly (via ODFS) using `OpenOrMoveFiles()` if the
// `user_email` matches the user email used in the
// `FakeProvidedFileSystemOneDrive`.
base::FilePath AndroidOneDrivePathToSharedDirectoryForEmail(
std::string user_email) {
// Filesystem path format is:
// /mount_path/Files
return AndroidOneDriveMountPathForEmail(user_email).Append("Files");
}
base::FilePath AndroidOneDriveMountPathForEmail(std::string user_email) {
return arc::GetDocumentsProviderMountPath(
"com.microsoft.skydrive.content.StorageAccessProvider", user_email);
}
void SetNetworkConnected(const bool connected) {
content::SetNetworkConnectionTrackerForTesting(nullptr);
content::SetNetworkConnectionTrackerForTesting(connection_tracker_.get());
using enum network::mojom::ConnectionType;
connection_tracker_->SetConnectionType(connected ? CONNECTION_WIFI
: CONNECTION_NONE);
SetDriveConnectionStatusForTesting(connected
? ConnectionStatus::kConnected
: ConnectionStatus::kNoNetwork);
}
// Record the notification message shown.
void OnNotificationDisplayed(
const message_center::Notification& notification,
const NotificationCommon::Metadata* const metadata) override {
notification_title_ = base::UTF16ToUTF8(notification.title());
notification_message_ = base::UTF16ToUTF8(notification.message());
notification_warning_level_ =
notification.system_notification_warning_level();
}
void OnNotificationClosed(const std::string& notification_id) override {}
void OnNotificationDisplayServiceDestroyed(
NotificationDisplayService* service) override {}
protected:
std::string notification_title_;
std::string notification_message_;
message_center::SystemNotificationWarningLevel notification_warning_level_;
FileSystemURL odfs_docx_test_file_url_1_;
FileSystemURL odfs_pptx_test_file_url_2_;
std::unique_ptr<FakeWebAppPublisher> web_app_publisher_;
base::FilePath relative_test_path_1_;
base::FilePath relative_test_path_2_;
base::FilePath test_path_within_odfs_1_;
base::FilePath test_path_within_odfs_2_;
const TaskDescriptor open_in_office_task_ = CreateOpenInOfficeTask();
std::vector<storage::FileSystemURL> file_urls_;
raw_ptr<test::FakeProvidedFileSystemOneDrive,
DanglingUntriaged>
provided_file_system_; // Owned by Service.
const blink::StorageKey kTestStorageKey =
blink::StorageKey::CreateFromStringForTesting("chrome://abc");
base::HistogramTester histogram_;
std::unique_ptr<ash::cloud_upload::CloudOpenMetrics> cloud_open_metrics_;
private:
base::test::ScopedFeatureList feature_list_;
const std::unique_ptr<TestNetworkConnectionTracker> connection_tracker_ =
TestNetworkConnectionTracker::CreateInstance();
const std::string test_docx_file_name_1_ = "text.docx";
const std::string test_pptx_file_name_2_ = "presentation.pptx";
base::WeakPtr<ash::cloud_upload::CloudOpenMetrics>
cloud_open_metrics_weak_ptr_;
};
#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(OneDriveTest, OfficeFallbackTryAgain) {
// Creates a fake ODFS with a test file and disable the setup flow for office
// files because we want the office fallback dialog to run instead. Set no
// connection to trigger the fallback flow.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false,
/*connect_to_network=*/false);
// 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();
web_app_publisher_->ClearPastLaunches();
// Launches the office fallback dialog as the system is offline.
base::test::TestFuture<TaskResult, std::string> executed_future;
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
executed_future.GetCallback());
ASSERT_EQ(executed_future.Get<0>(), TaskResult::kOpened);
// Wait for office fallback dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
CHECK_EQ(0u, web_app_publisher_->GetLaunches().size());
SetNetworkConnected(true);
// Run dialog callback, simulate user choosing to "try-again". Will succeed
// because system is online, and the file doesn't need to be moved.
OnDialogChoiceReceived(profile(), open_in_office_task_, file_urls_,
ash::office_fallback::FallbackReason::kOffline,
std::move(cloud_open_metrics_),
ash::office_fallback::kDialogChoiceTryAgain);
auto launches = web_app_publisher_->GetLaunches();
ASSERT_EQ(1u, launches.size());
CHECK_EQ(launches[0].app_id, web_app::kMicrosoft365AppId);
CHECK_EQ(launches[0].intent_url, test::kODFSSampleUrl);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kNumberOfFilesToOpenWithOneDriveMetric, 1, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kOneDrive, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveOpenSourceVolumeMetric,
ash::cloud_upload::OfficeFilesSourceVolume::kMicrosoftOneDrive, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kNotRequired, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kOpened, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveErrorMetricName,
ash::cloud_upload::OfficeOneDriveOpenErrors::kSuccess, 1);
}
// 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(OneDriveTest, OfficeFallbackCancel) {
// Creates a fake ODFS with a test file and and disable the setup flow for
// office files because we want the office fallback dialog to run instead. Set
// no connection to trigger the fallback flow.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false,
/*connect_to_network=*/false);
// 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();
web_app_publisher_->ClearPastLaunches();
// Launches the office fallback dialog as the system is offline.
base::test::TestFuture<TaskResult, std::string> executed_future;
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
executed_future.GetCallback());
ASSERT_EQ(executed_future.Get<0>(), TaskResult::kOpened);
// Wait for office fallback dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
ASSERT_EQ(0u, web_app_publisher_->GetLaunches().size());
SetNetworkConnected(true);
// Run dialog callback, simulate user choosing to "cancel". The file will not
// open.
OnDialogChoiceReceived(profile(), open_in_office_task_, file_urls_,
ash::office_fallback::FallbackReason::kOffline,
std::move(cloud_open_metrics_),
ash::office_fallback::kDialogChoiceCancel);
ASSERT_EQ(0u, web_app_publisher_->GetLaunches().size());
histogram_.ExpectUniqueSample(
ash::cloud_upload::kNumberOfFilesToOpenWithOneDriveMetric, 1, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kOneDrive, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kCancelledAtFallback, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveErrorMetricName,
ash::cloud_upload::OfficeOneDriveOpenErrors::kOffline, 1);
}
// Test to check that a second office fallback dialog will not launch when one
// is already being shown.
IN_PROC_BROWSER_TEST_F(OneDriveTest, CannotGetOfficeFallbackChoice) {
// Creates a fake ODFS with a test file and and disable the setup flow for
// office files because we want the office fallback dialog to run instead. Set
// no connection to trigger the fallback flow.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false,
/*connect_to_network=*/false);
// 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();
web_app_publisher_->ClearPastLaunches();
// Launches the first office fallback dialog as the system is offline. Let it
// hang waiting for a choice from the user.
base::test::TestFuture<TaskResult, std::string> executed_future;
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
executed_future.GetCallback());
ASSERT_EQ(executed_future.Get<0>(), TaskResult::kOpened);
// Wait for the first office fallback dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
// Fails to launch a second office fallback dialog.
base::test::TestFuture<TaskResult, std::string> failed_future;
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
failed_future.GetCallback());
ASSERT_EQ(failed_future.Get<0>(), TaskResult::kFailed);
// Both open file requests will log the CloudProvider metric.
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kOneDrive, 2);
// Only the second open file request will complete. Expect that the second
// office fallback dialog couldn't be opened.
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kCannotGetFallbackChoice, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveErrorMetricName,
ash::cloud_upload::OfficeOneDriveOpenErrors::kOffline, 1);
}
// Tests that opening a file from Android OneDrive under a virtual folder that
// we don't support (Recents, Albums etc...) shows the fallback dialog.
IN_PROC_BROWSER_TEST_F(
OneDriveTest,
FailToOpenFileFromAndroidOneDriveDirectoryNotAccessibleToODFS) {
// Create a fake ODFS with a test file.
SetUpTest(/*disable_set_up=*/false, /*launch_files_app=*/false);
// Create file path in Android OneDrive in a virtual folder that is not
// supported in ODFS.
base::FilePath android_onedrive_path_no_equivalent =
AndroidOneDriveMountPathForEmail(test::kSampleUserEmail1)
.Append("Shared")
.Append(relative_test_path_1_);
// Create a FileSystemURL from the Android OneDrive file path.
FileSystemURL android_onedrive_url = FileSystemURL::CreateForTest(
kTestStorageKey, storage::kFileSystemTypeArcDocumentsProvider,
android_onedrive_path_no_equivalent);
// 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();
// Attempt to open the file indirectly from Android OneDrive (via ODFS). It
// will fail as there is not an equivalent ODFS file path.
auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
profile(), {android_onedrive_url}, open_in_office_task_,
ash::cloud_upload::CloudProvider::kOneDrive,
std::make_unique<ash::cloud_upload::CloudOpenMetrics>(
ash::cloud_upload::CloudProvider::kOneDrive, 1)));
task->OpenOrMoveFiles();
// Wait for office fallback dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
// Get the web contents of the fallback dialog.
content::WebContents* web_contents =
GetWebContentsFromOfficeFallbackAndWaitForDialog();
EXPECT_TRUE(content::ExecJs(web_contents,
"document.querySelector('office-fallback')"
".$('#ok-button').click()"));
base::RunLoop run_loop;
WebContentsDestroyedWaiter waiter(web_contents, run_loop.QuitClosure());
run_loop.Run();
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kNotRequired, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kOkAtFallbackAfterOpen, 1);
// Expect the conversion to an ODFS equivalent URL to fail.
histogram_.ExpectUniqueSample(ash::cloud_upload::kOneDriveErrorMetricName,
ash::cloud_upload::OfficeOneDriveOpenErrors::
kAndroidOneDriveUnsupportedLocation,
1);
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
// Test to check that a second setup dialog will not launch when one
// is already being shown for a different file.
IN_PROC_BROWSER_TEST_F(OneDriveTest, CannotShowDifferentSetupDialog) {
// Creates a fake ODFS with a test file.
SetUpTest(/*disable_set_up=*/false, /*launch_files_app=*/true);
std::vector<storage::FileSystemURL> file_urls1{odfs_docx_test_file_url_1_};
std::vector<storage::FileSystemURL> file_urls2{odfs_pptx_test_file_url_2_};
// Watch for dialog URL chrome://cloud-upload.
GURL expected_dialog_URL(chrome::kChromeUICloudUploadURL);
content::TestNavigationObserver navigation_observer_dialog(
expected_dialog_URL);
navigation_observer_dialog.StartWatchingNewWebContents();
// Launches the first setup dialog (file handler dialog). Let it
// hang waiting for a choice from the user.
base::test::TestFuture<TaskResult, std::string> executed_future;
ExecuteFileTask(profile(), open_in_office_task_, file_urls1,
executed_future.GetCallback());
ASSERT_EQ(executed_future.Get<0>(), TaskResult::kOpened);
// Wait for the first setup dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
// Fails to launch a second setup dialog for a different file.
base::test::TestFuture<TaskResult, std::string> failed_future;
ExecuteFileTask(profile(), open_in_office_task_, file_urls2,
failed_future.GetCallback());
ASSERT_EQ(failed_future.Get<0>(), TaskResult::kFailed);
// Both open file requests will log the CloudProvider metric.
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kOneDrive, 2);
// Only the second open file request will complete and with a
// kCannotShowSetupDialog TaskResult.
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kCannotShowSetupDialog, 1);
}
// Test to check that a second move confirmation dialog will not launch when one
// is already being shown for a different file.
IN_PROC_BROWSER_TEST_F(OneDriveTest, CannotShowDifferentMoveConfirmation) {
// Creates a fake ODFS.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/true);
FileSystemURL file_outside_one_drive1 =
CreateOfficeFileSourceURL(profile(), "text.docx");
FileSystemURL file_outside_one_drive2 =
CreateOfficeFileSourceURL(profile(), "text2.docx");
std::vector<storage::FileSystemURL> file_urls1{file_outside_one_drive1};
std::vector<storage::FileSystemURL> file_urls2{file_outside_one_drive2};
// Watch for dialog URL chrome://cloud-upload.
GURL expected_dialog_URL(chrome::kChromeUICloudUploadURL);
content::TestNavigationObserver navigation_observer_dialog(
expected_dialog_URL);
navigation_observer_dialog.StartWatchingNewWebContents();
// Launches the first move confirmation dialog. Let it
// hang waiting for a choice from the user.
base::test::TestFuture<TaskResult, std::string> executed_future;
ExecuteFileTask(profile(), open_in_office_task_, file_urls1,
executed_future.GetCallback());
ASSERT_EQ(executed_future.Get<0>(), TaskResult::kOpened);
// Wait for the first move confirmation dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
// Fails to launch a second move confirmation dialog for a different file.
base::test::TestFuture<TaskResult, std::string> failed_future;
ExecuteFileTask(profile(), open_in_office_task_, file_urls2,
failed_future.GetCallback());
ASSERT_EQ(failed_future.Get<0>(), TaskResult::kFailed);
// Both requests will log the TransferRequired metric.
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kMove, 2);
// Only the second open file request will complete and with a
// kCannotShowMoveConfirmation TaskResult.
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kCannotShowMoveConfirmation, 1);
}
// Test to check that subsequent setup dialogs will not launch when one
// is already being shown for the same file.
IN_PROC_BROWSER_TEST_F(OneDriveTest, CannotShowDuplicateSetupDialogs) {
// Creates a fake ODFS with a test file.
SetUpTest(/*disable_set_up=*/false, /*launch_files_app=*/true);
// Watch for dialog URL chrome://cloud-upload.
GURL expected_dialog_URL(chrome::kChromeUICloudUploadURL);
content::TestNavigationObserver navigation_observer_dialog(
expected_dialog_URL);
navigation_observer_dialog.StartWatchingNewWebContents();
// Launches the first setup dialog (file handler dialog). Let it
// hang waiting for a choice from the user.
base::test::TestFuture<TaskResult, std::string> executed_future;
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
executed_future.GetCallback());
ASSERT_EQ(executed_future.Get<0>(), TaskResult::kOpened);
// Wait for the first setup dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
NotificationDisplayService::GetForProfile(profile())->AddObserver(this);
// Fails to launch a second setup dialog for the same file.
base::test::TestFuture<TaskResult, std::string> failed_future;
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
failed_future.GetCallback());
ASSERT_EQ(failed_future.Get<0>(), TaskResult::kFailed);
// Expect that the file already being opened notification was shown.
EXPECT_EQ(notification_title_,
ash::cloud_upload::GetAlreadyBeingOpenedTitle());
EXPECT_EQ(notification_message_,
ash::cloud_upload::GetAlreadyBeingOpenedMessage());
EXPECT_EQ(notification_warning_level_,
message_center::SystemNotificationWarningLevel::NORMAL);
// Fails to launch a third setup dialog for the same file.
base::test::TestFuture<TaskResult, std::string> failed_future2;
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
failed_future2.GetCallback());
ASSERT_EQ(failed_future2.Get<0>(), TaskResult::kFailed);
// All open file requests will log the CloudProvider metric.
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kOneDrive, 3);
// Only the subsequent open file requests will complete and with a
// kFileAlreadyBeingOpened TaskResult.
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kFileAlreadyBeingOpened, 2);
}
// Test to check that a second move confirmation dialog will not launch when one
// is already being shown for the same file.
IN_PROC_BROWSER_TEST_F(OneDriveTest, CannotShowDuplicateMoveConfirmation) {
// Creates a fake ODFS.
SetUpTest(/*disable_set_up=*/false, /*launch_files_app=*/true);
FileSystemURL file_outside_one_drive = CreateOfficeFileSourceURL(profile());
std::vector<storage::FileSystemURL> file_urls{file_outside_one_drive};
// Disable the setup flow for office files.
SetWordFileHandlerToFilesSWA(profile(), kActionIdWebDriveOfficeWord);
// Watch for dialog URL chrome://cloud-upload.
GURL expected_dialog_URL(chrome::kChromeUICloudUploadURL);
content::TestNavigationObserver navigation_observer_dialog(
expected_dialog_URL);
navigation_observer_dialog.StartWatchingNewWebContents();
// Launches the first move confirmation dialog. Let it
// hang waiting for a choice from the user.
base::test::TestFuture<TaskResult, std::string> executed_future;
ExecuteFileTask(profile(), open_in_office_task_, file_urls,
executed_future.GetCallback());
ASSERT_EQ(executed_future.Get<0>(), TaskResult::kOpened);
// Wait for the first move confirmation dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
NotificationDisplayService::GetForProfile(profile())->AddObserver(this);
// Fails to launch a second move confirmation dialog for the same file.
base::test::TestFuture<TaskResult, std::string> failed_future;
ExecuteFileTask(profile(), open_in_office_task_, file_urls,
failed_future.GetCallback());
ASSERT_EQ(failed_future.Get<0>(), TaskResult::kFailed);
// Expect that the file already being opened notification was shown.
EXPECT_EQ(notification_title_,
ash::cloud_upload::GetAlreadyBeingOpenedTitle());
EXPECT_EQ(notification_message_,
ash::cloud_upload::GetAlreadyBeingOpenedMessage());
EXPECT_EQ(notification_warning_level_,
message_center::SystemNotificationWarningLevel::NORMAL);
// Only the first file request will log the TransferRequired metric.
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kMove, 1);
// Only the second open file request will complete and with a
// kFileAlreadyBeingOpened TaskResult.
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kFileAlreadyBeingOpened, 1);
}
// Test that ExecuteFileTask() will open an ODFS office file with the Open In
// Office task whilst an unrelated file is being opened.
IN_PROC_BROWSER_TEST_F(OneDriveTest, OpenDifferentFileFromODFS) {
// Creates a fake ODFS with a test file.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false);
web_app_publisher_->ClearPastLaunches();
// Add a current CloudOpenTask for unrelated file.
auto* event_router =
file_manager::EventRouterFactory::GetForProfile(profile());
ASSERT_TRUE(event_router);
event_router->AddCloudOpenTask(odfs_pptx_test_file_url_2_);
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
base::DoNothing());
auto launches = web_app_publisher_->GetLaunches();
ASSERT_EQ(1u, launches.size());
CHECK_EQ(launches[0].app_id, web_app::kMicrosoft365AppId);
CHECK_EQ(launches[0].intent_url, test::kODFSSampleUrl);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kNumberOfFilesToOpenWithOneDriveMetric, 1, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kOneDrive, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveOpenSourceVolumeMetric,
ash::cloud_upload::OfficeFilesSourceVolume::kMicrosoftOneDrive, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kNotRequired, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kOpened, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveErrorMetricName,
ash::cloud_upload::OfficeOneDriveOpenErrors::kSuccess, 1);
}
// Test that ExecuteFileTask() will open the same ODFS office file twice in a
// row.
IN_PROC_BROWSER_TEST_F(OneDriveTest, OpenSameFileFromODFSTwiceInARow) {
// Creates a fake ODFS with a test file.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false);
web_app_publisher_->ClearPastLaunches();
// Open the file.
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
base::DoNothing());
auto launches = web_app_publisher_->GetLaunches();
ASSERT_EQ(1u, launches.size());
CHECK_EQ(launches[0].app_id, web_app::kMicrosoft365AppId);
CHECK_EQ(launches[0].intent_url, test::kODFSSampleUrl);
// Open the file twice in a row. This should not be blocked since the opens
// are not overlapping.
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
base::DoNothing());
launches = web_app_publisher_->GetLaunches();
ASSERT_EQ(2u, launches.size());
CHECK_EQ(launches[1].app_id, web_app::kMicrosoft365AppId);
CHECK_EQ(launches[1].intent_url, test::kODFSSampleUrl);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kNumberOfFilesToOpenWithOneDriveMetric, 1, 2);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kOneDrive, 2);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveOpenSourceVolumeMetric,
ash::cloud_upload::OfficeFilesSourceVolume::kMicrosoftOneDrive, 2);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kNotRequired, 2);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kOpened, 2);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveErrorMetricName,
ash::cloud_upload::OfficeOneDriveOpenErrors::kSuccess, 2);
}
// Test that ExecuteFileTask() will not open an ODFS office file when that file
// is already being opened.
IN_PROC_BROWSER_TEST_F(OneDriveTest,
CannotOpenFileFromODFSWhenAlreadyBeingOpened) {
// Creates a fake ODFS with a test file.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false);
web_app_publisher_->ClearPastLaunches();
// Add a current CloudOpenTask for the file.
auto* event_router =
file_manager::EventRouterFactory::GetForProfile(profile());
ASSERT_TRUE(event_router);
event_router->AddCloudOpenTask(odfs_docx_test_file_url_1_);
NotificationDisplayService::GetForProfile(profile())->AddObserver(this);
// Fail to execute a duplicate CloudOpenTask for the file.
base::test::TestFuture<TaskResult, std::string> failed_future;
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
failed_future.GetCallback());
ASSERT_EQ(failed_future.Get<0>(), TaskResult::kFailed);
// Expect that the file already being opened notification was shown.
EXPECT_EQ(notification_title_,
ash::cloud_upload::GetAlreadyBeingOpenedTitle());
EXPECT_EQ(notification_message_,
ash::cloud_upload::GetAlreadyBeingOpenedMessage());
EXPECT_EQ(notification_warning_level_,
message_center::SystemNotificationWarningLevel::NORMAL);
// Fail again to execute a duplicate CloudOpenTask for the file.
base::test::TestFuture<TaskResult, std::string> failed_future2;
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
failed_future2.GetCallback());
ASSERT_EQ(failed_future2.Get<0>(), TaskResult::kFailed);
auto launches = web_app_publisher_->GetLaunches();
ASSERT_EQ(0u, launches.size());
histogram_.ExpectUniqueSample(
ash::cloud_upload::kNumberOfFilesToOpenWithOneDriveMetric, 1, 2);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kOneDrive, 2);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kFileAlreadyBeingOpened, 2);
}
// Test that ExecuteFileTask() will open ODFS office files with the Open In
// Office task.
// TODO(b/242685536) add support for multiple files and reenable this test.
IN_PROC_BROWSER_TEST_F(OneDriveTest, DISABLED_OpenFilesFromODFS) {
// Creates a fake ODFS with test files.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false);
web_app_publisher_->ClearPastLaunches();
file_urls_.push_back(odfs_pptx_test_file_url_2_);
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
base::DoNothing());
auto launches = web_app_publisher_->GetLaunches();
ASSERT_EQ(2u, launches.size());
CHECK_EQ(launches[0].app_id, web_app::kMicrosoft365AppId);
CHECK_EQ(launches[0].intent_url, test::kODFSSampleUrl);
CHECK_EQ(launches[1].app_id, web_app::kMicrosoft365AppId);
CHECK_EQ(launches[1].intent_url, test::kODFSSampleUrl);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kNumberOfFilesToOpenWithOneDriveMetric, 2, 1);
// TODO(b/325514165): Expect 2 CloudProvider, SourceVolume and
// TransferRequired metrics once a metric is logged for each file open.
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kOneDrive, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveOpenSourceVolumeMetric,
ash::cloud_upload::OfficeFilesSourceVolume::kMicrosoftOneDrive, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kNotRequired, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kOpened, 2);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveErrorMetricName,
ash::cloud_upload::OfficeOneDriveOpenErrors::kSuccess, 2);
}
// Test that OpenOrMoveFiles() will open the Move Confirmation dialog when the
// cloud provider specified is OneDrive but the office file to be opened needs
// to be moved to ODFS.
IN_PROC_BROWSER_TEST_F(OneDriveTest, OpenFileNotFromODFS) {
FileSystemURL file_outside_one_drive = CreateOfficeFileSourceURL(profile());
std::vector<storage::FileSystemURL> file_urls{file_outside_one_drive};
// Watch for dialog URL chrome://cloud-upload.
GURL expected_dialog_URL(chrome::kChromeUICloudUploadURL);
content::TestNavigationObserver navigation_observer_dialog(
expected_dialog_URL);
navigation_observer_dialog.StartWatchingNewWebContents();
LaunchFilesAppAndWait();
// Triggers Move Confirmation dialog.
auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
profile(), file_urls, CreateOpenInOfficeTask(),
ash::cloud_upload::CloudProvider::kOneDrive,
std::move(cloud_open_metrics_)));
task->OpenOrMoveFiles();
// Wait for Move Confirmation dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveOpenSourceVolumeMetric,
ash::cloud_upload::OfficeFilesSourceVolume::kDownloadsDirectory, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kMove, 1);
}
// Test that when opening a file from ODFS fails due reauthentication to
// OneDrive being required, the reauthentication required notification is shown.
IN_PROC_BROWSER_TEST_F(OneDriveTest,
FailToOpenFileFromODFSReauthenticationRequired) {
// Creates a fake ODFS with a test file.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false);
// Ensure the open fails due to reauthentication to OneDrive being required.
provided_file_system_->SetGetActionsError(
base::File::Error::FILE_ERROR_ACCESS_DENIED);
provided_file_system_->SetReauthenticationRequired(true);
NotificationDisplayService::GetForProfile(profile())->AddObserver(this);
web_app_publisher_->ClearPastLaunches();
// Open file directly from ODFS.
auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
profile(), file_urls_, open_in_office_task_,
ash::cloud_upload::CloudProvider::kOneDrive,
std::move(cloud_open_metrics_)));
task->OpenOrMoveFiles();
// Expect that the reauthentication required notification was shown.
EXPECT_EQ(notification_message_,
ash::cloud_upload::GetReauthenticationRequiredMessage());
EXPECT_EQ(notification_warning_level_,
message_center::SystemNotificationWarningLevel::WARNING);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveOpenSourceVolumeMetric,
ash::cloud_upload::OfficeFilesSourceVolume::kMicrosoftOneDrive, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kNotRequired, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kFailedToOpen, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveErrorMetricName,
ash::cloud_upload::OfficeOneDriveOpenErrors::kGetActionsReauthRequired,
1);
NotificationDisplayService::GetForProfile(browser()->profile())
->RemoveObserver(this);
}
// Test that when opening a file from ODFS fails due an access error that is not
// because reauthentication to OneDrive is required, the generic error
// notification is shown.
IN_PROC_BROWSER_TEST_F(OneDriveTest, FailToOpenFileFromODFSOtherAccessError) {
// Creates a fake ODFS with a test file.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false);
// Ensure the open fails due to some access error which is not because
// reauthentication to OneDrive is required.
provided_file_system_->SetGetActionsError(
base::File::Error::FILE_ERROR_ACCESS_DENIED);
provided_file_system_->SetReauthenticationRequired(false);
NotificationDisplayService::GetForProfile(profile())->AddObserver(this);
web_app_publisher_->ClearPastLaunches();
// Open file directly from ODFS.
auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
profile(), file_urls_, open_in_office_task_,
ash::cloud_upload::CloudProvider::kOneDrive,
std::move(cloud_open_metrics_)));
task->OpenOrMoveFiles();
// Expect that the reauthentication required notification was shown.
EXPECT_EQ(notification_message_, ash::cloud_upload::GetGenericErrorMessage());
EXPECT_EQ(notification_warning_level_,
message_center::SystemNotificationWarningLevel::WARNING);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveOpenSourceVolumeMetric,
ash::cloud_upload::OfficeFilesSourceVolume::kMicrosoftOneDrive, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kNotRequired, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kFailedToOpen, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveErrorMetricName,
ash::cloud_upload::OfficeOneDriveOpenErrors::kGetActionsAccessDenied, 1);
NotificationDisplayService::GetForProfile(browser()->profile())
->RemoveObserver(this);
}
// Test that OpenOrMoveFiles() will open an Android OneDrive office file via
// ODFS when the cloud provider specified is OneDrive. Test that the file path
// is not on ODFS but does get opened via ODFS.
IN_PROC_BROWSER_TEST_F(OneDriveTest, OpenFileFromAndroidOneDriveViaODFS) {
// Create a fake ODFS with a test file.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false);
// Create equivalent file path in Android OneDrive with same email (email
// matches `FakeProvidedFileSystemOneDrive`).
base::FilePath android_onedrive_path_same_email =
AndroidOneDrivePathToSharedDirectoryForEmail(test::kSampleUserEmail1)
.Append(relative_test_path_1_);
// Create a FileSystemURL from the Android OneDrive file path.
FileSystemURL android_onedrive_url = FileSystemURL::CreateForTest(
kTestStorageKey, storage::kFileSystemTypeArcDocumentsProvider,
android_onedrive_path_same_email);
web_app_publisher_->ClearPastLaunches();
// Open the file indirectly from Android OneDrive (via ODFS).
auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
profile(), {android_onedrive_url}, open_in_office_task_,
ash::cloud_upload::CloudProvider::kOneDrive,
std::move(cloud_open_metrics_)));
task->OpenOrMoveFiles();
auto launches = web_app_publisher_->GetLaunches();
ASSERT_EQ(1u, launches.size());
CHECK_EQ(launches[0].app_id, web_app::kMicrosoft365AppId);
// Check that the ODFS URL was opened.
CHECK_EQ(launches[0].intent_url, test::kODFSSampleUrl);
// Expect the source volume not to be ODFS. Logged as kUnknown because would
// need to actually mount Android OneDrive Documents Provider for
// VOLUME_TYPE_DOCUMENTS_PROVIDER to be logged in the test.
histogram_.ExpectBucketCount(
ash::cloud_upload::kOneDriveOpenSourceVolumeMetric,
ash::cloud_upload::OfficeFilesSourceVolume::kMicrosoftOneDrive, 0);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kNotRequired, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kOpened, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveErrorMetricName,
ash::cloud_upload::OfficeOneDriveOpenErrors::kSuccess, 1);
}
// Same as OpenFileFromAndroidOneDriveViaODFS test the email account associated
// with Android OneDrive is the upper case version of the one associated with
// ODFS.
IN_PROC_BROWSER_TEST_F(OneDriveTest,
OpenFileFromAndroidOneDriveViaODFSDiffCaseEmail) {
// Create a fake ODFS with a test file.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false);
// Create equivalent file path in Android OneDrive with an upper case version
// of the same email (email matches `FakeProvidedFileSystemOneDrive`).
base::FilePath android_onedrive_path_same_email =
AndroidOneDrivePathToSharedDirectoryForEmail(
test::kSampleUserUpperCaseEmail1)
.Append(relative_test_path_1_);
// Create a FileSystemURL from the Android OneDrive file path.
FileSystemURL android_onedrive_url = FileSystemURL::CreateForTest(
kTestStorageKey, storage::kFileSystemTypeArcDocumentsProvider,
android_onedrive_path_same_email);
web_app_publisher_->ClearPastLaunches();
// Open the file indirectly from Android OneDrive (via ODFS).
auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
profile(), {android_onedrive_url}, open_in_office_task_,
ash::cloud_upload::CloudProvider::kOneDrive,
std::move(cloud_open_metrics_)));
task->OpenOrMoveFiles();
auto launches = web_app_publisher_->GetLaunches();
ASSERT_EQ(1u, launches.size());
CHECK_EQ(launches[0].app_id, web_app::kMicrosoft365AppId);
// Check that the ODFS URL was opened.
CHECK_EQ(launches[0].intent_url, test::kODFSSampleUrl);
// Expect the source volume not to be ODFS. Logged as kUnknown because would
// need to actually mount Android OneDrive Documents Provider for
// VOLUME_TYPE_DOCUMENTS_PROVIDER to be logged in the test.
histogram_.ExpectBucketCount(
ash::cloud_upload::kOneDriveOpenSourceVolumeMetric,
ash::cloud_upload::OfficeFilesSourceVolume::kMicrosoftOneDrive, 0);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kNotRequired, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kOpened, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveErrorMetricName,
ash::cloud_upload::OfficeOneDriveOpenErrors::kSuccess, 1);
}
// Test that OpenOrMoveFiles() will not open an Android OneDrive office file via
// ODFS when the cloud provider specified is OneDrive but the email accounts for
// ODFS and Android OneDrive don't match. The Android OneDrive file path will
// convert to a valid ODFS file path but as the email accounts don't match, the
// file won't open.
IN_PROC_BROWSER_TEST_F(OneDriveTest,
FailToOpenFileFromAndroidOneDriveViaODFSDiffEmail) {
// Create a fake ODFS with a test file.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false);
// Create equivalent file path in Android OneDrive with different email
// (email doesn't match `FakeProvidedFileSystemOneDrive`).
base::FilePath android_onedrive_path_diff_email =
AndroidOneDrivePathToSharedDirectoryForEmail(test::kSampleUserEmail2)
.Append(relative_test_path_1_);
// Create a FileSystemURL from the Android OneDrive file path.
FileSystemURL android_onedrive_url = FileSystemURL::CreateForTest(
kTestStorageKey, storage::kFileSystemTypeArcDocumentsProvider,
android_onedrive_path_diff_email);
web_app_publisher_->ClearPastLaunches();
// Attempt to open the file indirectly from Android OneDrive (via ODFS). It
// will fail as the email accounts don't match.
auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
profile(), {android_onedrive_url}, open_in_office_task_,
ash::cloud_upload::CloudProvider::kOneDrive,
std::move(cloud_open_metrics_)));
task->OpenOrMoveFiles();
auto launches = web_app_publisher_->GetLaunches();
ASSERT_EQ(0u, launches.size());
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kNotRequired, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kFailedToOpen, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveErrorMetricName,
ash::cloud_upload::OfficeOneDriveOpenErrors::kEmailsDoNotMatch, 1);
}
// Test that OpenOrMoveFiles() will not open an Android OneDrive office file
// (with the expected Url format) via ODFS when the cloud provider specified is
// OneDrive but the Android OneDrive path does not exist on ODFS.
IN_PROC_BROWSER_TEST_F(OneDriveTest,
FailToOpenFileFromAndroidOneDriveNotOnODFS) {
// Create a fake ODFS with a test file.
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false);
// Create file path in Android OneDrive that is in the "Files" directory but
// doesn't exist on ODFS.
base::FilePath android_onedrive_path_no_equivalent =
AndroidOneDrivePathToSharedDirectoryForEmail(test::kSampleUserEmail1)
.Append("another_file.docx");
// Create a FileSystemURL from the Android OneDrive file path.
FileSystemURL android_onedrive_url = FileSystemURL::CreateForTest(
kTestStorageKey, storage::kFileSystemTypeArcDocumentsProvider,
android_onedrive_path_no_equivalent);
web_app_publisher_->ClearPastLaunches();
// Attempt to open the file indirectly from Android OneDrive (via ODFS). It
// will fail as there is not an equivalent ODFS file path.
auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
profile(), {android_onedrive_url}, open_in_office_task_,
ash::cloud_upload::CloudProvider::kOneDrive,
std::move(cloud_open_metrics_)));
task->OpenOrMoveFiles();
auto launches = web_app_publisher_->GetLaunches();
ASSERT_EQ(0u, launches.size());
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTransferRequiredMetric,
ash::cloud_upload::OfficeFilesTransferRequired::kNotRequired, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveTaskResultMetricName,
ash::cloud_upload::OfficeTaskResult::kFailedToOpen, 1);
// Expect get actions error for a non-existent path.
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOneDriveErrorMetricName,
ash::cloud_upload::OfficeOneDriveOpenErrors::kGetActionsGenericError, 1);
}
// Test that the setup flow for office files, that has never been run before,
// will be run when an Open in Office task tries to open an office file
// already in ODFS.
IN_PROC_BROWSER_TEST_F(OneDriveTest, FileInOneDriveOpensSetUpDialog) {
// Creates a fake ODFS with a test file.
SetUpTest(/*disable_set_up=*/false, /*launch_files_app=*/true);
// Watch for dialog URL chrome://cloud-upload.
GURL expected_dialog_URL(chrome::kChromeUICloudUploadURL);
content::TestNavigationObserver navigation_observer_dialog(
expected_dialog_URL);
navigation_observer_dialog.StartWatchingNewWebContents();
// Triggers setup flow.
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
base::DoNothing());
// Wait for setup flow dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
histogram_.ExpectUniqueSample(
ash::cloud_upload::kNumberOfFilesToOpenWithOneDriveMetric, 1, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kOneDrive, 1);
}
// Test that the setup flow for office files, that has never been run before,
// will be run when an Open in Office task tries to open an office file not
// already in ODFS.
IN_PROC_BROWSER_TEST_F(OneDriveTest, FileNotInOneDriveOpensSetUpDialog) {
SetNetworkConnected(true);
// The file is not in the correct location for this task and would have to be
// moved to ODFS.
FileSystemURL file_outside_one_drive = CreateOfficeFileSourceURL(profile());
std::vector<storage::FileSystemURL> file_urls{file_outside_one_drive};
// Watch for dialog URL chrome://cloud-upload.
GURL expected_dialog_URL(chrome::kChromeUICloudUploadURL);
content::TestNavigationObserver navigation_observer_dialog(
expected_dialog_URL);
navigation_observer_dialog.StartWatchingNewWebContents();
LaunchFilesAppAndWait();
// Triggers setup flow.
ExecuteFileTask(profile(), open_in_office_task_, file_urls,
base::DoNothing());
// Wait for setup flow dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
histogram_.ExpectUniqueSample(
ash::cloud_upload::kNumberOfFilesToOpenWithOneDriveMetric, 1, 1);
histogram_.ExpectUniqueSample(
ash::cloud_upload::kOpenInitialCloudProviderMetric,
ash::cloud_upload::CloudProvider::kOneDrive, 1);
}
class OfficeDriveHatsSurvey : public DriveTest {
public:
OfficeDriveHatsSurvey() {
feature_list_.InitWithFeatures({chromeos::features::kUploadOfficeToCloud,
::features::kHappinessTrackingOffice},
{});
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Test that the Office HaTS survey for Drive gets triggered when an Office file
// gets opened in Drive.
IN_PROC_BROWSER_TEST_F(OfficeDriveHatsSurvey, OpenInDrive) {
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false);
// Install Google Docs PWA.
webapps::AppId docs_app_id = web_app::test::InstallDummyWebApp(
profile(), "Google Docs",
GURL("https://docs.google.com/document/?usp=installed_webapp"));
ASSERT_EQ(docs_app_id, web_app::kGoogleDocsAppId);
apps::AppReadinessWaiter(profile(), web_app::kGoogleDocsAppId).Await();
base::test::TestFuture<std::string, ash::cloud_upload::HatsOfficeLaunchingApp>
hats_survey_executed_future;
ash::cloud_upload::HatsOfficeTrigger::Get().SetShowSurveyCallbackForTesting(
hats_survey_executed_future.GetCallback());
auto expected_state = apps::InstanceState(apps::kStarted | apps::kRunning |
apps::kActive | apps::kVisible);
apps::AppInstanceWaiter waiter(
apps::AppServiceProxyFactory::GetForProfile(profile())
->InstanceRegistry(),
web_app::kGoogleDocsAppId, expected_state);
base::test::TestFuture<TaskResult, std::string> task_executed_future;
ExecuteFileTask(profile(), CreateWebDriveOfficeTask(), file_urls_,
task_executed_future.GetCallback());
CHECK_EQ(task_executed_future.Get<0>(), TaskResult::kOpened);
waiter.Await();
// Check that the Drive HaTS survey has been triggered.
const auto [app_id, launching_app] = hats_survey_executed_future.Get();
ASSERT_EQ(app_id, web_app::kGoogleDocsAppId);
ASSERT_EQ(launching_app, ash::cloud_upload::HatsOfficeLaunchingApp::kDrive);
}
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
class OfficeMS365HatsSurvey : public OneDriveTest {
public:
OfficeMS365HatsSurvey() {
feature_list_.InitWithFeatures({chromeos::features::kUploadOfficeToCloud,
::features::kHappinessTrackingOffice},
{});
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Test that the right HaTS survey gets triggered when an Office file gets
// opened in MS365.
IN_PROC_BROWSER_TEST_F(OfficeMS365HatsSurvey, OpenInMS365) {
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false);
base::test::TestFuture<std::string, ash::cloud_upload::HatsOfficeLaunchingApp>
hats_survey_executed_future;
ash::cloud_upload::HatsOfficeTrigger::Get().SetShowSurveyCallbackForTesting(
hats_survey_executed_future.GetCallback());
base::test::TestFuture<TaskResult, std::string> task_executed_future;
ExecuteFileTask(profile(), CreateOpenInOfficeTask(), file_urls_,
task_executed_future.GetCallback());
CHECK_EQ(task_executed_future.Get<0>(), TaskResult::kOpened);
// Check that the MS365 HaTS survey has been triggered.
const auto [app_id, launching_app] = hats_survey_executed_future.Get();
ASSERT_EQ(app_id, web_app::kMicrosoft365AppId);
ASSERT_EQ(launching_app, ash::cloud_upload::HatsOfficeLaunchingApp::kMS365);
}
// Test that the right HaTS survey for gets triggered when an Office file gets
// opened in QuickOffice through the fallback dialog.
IN_PROC_BROWSER_TEST_F(OfficeMS365HatsSurvey, FallbackQuickOffice) {
SetUpTest(/*disable_set_up=*/true, /*launch_files_app=*/false,
/*connect_to_network=*/false);
// 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();
// Launches the office fallback dialog as the system is offline.
base::test::TestFuture<TaskResult, std::string> task_executed_future;
ExecuteFileTask(profile(), open_in_office_task_, file_urls_,
task_executed_future.GetCallback());
CHECK_EQ(task_executed_future.Get<0>(), TaskResult::kOpened);
// Wait for office fallback dialog to open.
navigation_observer_dialog.Wait();
ASSERT_TRUE(navigation_observer_dialog.last_navigation_succeeded());
base::test::TestFuture<std::string, ash::cloud_upload::HatsOfficeLaunchingApp>
hats_survey_executed_future;
ash::cloud_upload::HatsOfficeTrigger::Get().SetShowSurveyCallbackForTesting(
hats_survey_executed_future.GetCallback());
// Run dialog callback, with the "quick-office" option.
OnDialogChoiceReceived(profile(), open_in_office_task_, file_urls_,
ash::office_fallback::FallbackReason::kOffline,
std::move(cloud_open_metrics_),
ash::office_fallback::kDialogChoiceQuickOffice);
// Check that the QuickOffice HaTS survey has been triggered.
const auto [app_id, launching_app] = hats_survey_executed_future.Get();
ASSERT_EQ(app_id, std::string());
ASSERT_EQ(launching_app,
ash::cloud_upload::HatsOfficeLaunchingApp::kQuickOffice);
}
class OfficeQuickOfficeHatsSurveyClippyOn : public InProcessBrowserTest {
public:
OfficeQuickOfficeHatsSurveyClippyOn() {
feature_list_.InitWithFeatures({chromeos::features::kUploadOfficeToCloud,
::features::kHappinessTrackingOffice},
{});
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Test that the right HaTS survey gets triggered when an Office file gets
// opened in QuickOffice and the Clippy flag is enabled.
IN_PROC_BROWSER_TEST_F(OfficeQuickOfficeHatsSurveyClippyOn, OpenInQuickOffice) {
storage::FileSystemURL test_url;
std::vector<FileSystemURL> file_url{test_url};
base::test::TestFuture<std::string, ash::cloud_upload::HatsOfficeLaunchingApp>
hats_survey_executed_future;
ash::cloud_upload::HatsOfficeTrigger::Get().SetShowSurveyCallbackForTesting(
hats_survey_executed_future.GetCallback());
file_manager::file_tasks::LaunchQuickOffice(browser()->profile(), file_url);
const auto [app_id, launching_app] = hats_survey_executed_future.Get();
ASSERT_EQ(app_id, std::string());
ASSERT_EQ(launching_app,
ash::cloud_upload::HatsOfficeLaunchingApp::kQuickOffice);
}
class OfficeQuickOfficeHatsSurveyClippyOff : public InProcessBrowserTest {
public:
OfficeQuickOfficeHatsSurveyClippyOff() {
feature_list_.InitWithFeatures({::features::kHappinessTrackingOffice},
{chromeos::features::kUploadOfficeToCloud});
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Test that the right HaTS survey gets triggered when an Office file gets
// opened in QuickOffice and the Clippy flag is disabled.
IN_PROC_BROWSER_TEST_F(OfficeQuickOfficeHatsSurveyClippyOff,
OpenInQuickOffice) {
storage::FileSystemURL test_url;
std::vector<FileSystemURL> file_url{test_url};
base::test::TestFuture<std::string, ash::cloud_upload::HatsOfficeLaunchingApp>
hats_survey_executed_future;
ash::cloud_upload::HatsOfficeTrigger::Get().SetShowSurveyCallbackForTesting(
hats_survey_executed_future.GetCallback());
file_manager::file_tasks::LaunchQuickOffice(browser()->profile(), file_url);
const auto [app_id, launching_app] = hats_survey_executed_future.Get();
ASSERT_EQ(app_id, std::string());
ASSERT_EQ(launching_app,
ash::cloud_upload::HatsOfficeLaunchingApp::kQuickOfficeClippyOff);
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
INSTANTIATE_SYSTEM_WEB_APP_MANAGER_TEST_SUITE_ALL_PROFILE_TYPES_P(
FileTasksBrowserTest);
INSTANTIATE_SYSTEM_WEB_APP_MANAGER_TEST_SUITE_ALL_PROFILE_TYPES_P(
FileTasksPolicyBrowserTest);
} // namespace file_manager::file_tasks