| // Copyright 2024 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/policy/skyvault/drive_skyvault_uploader.h" |
| |
| #include <memory> |
| #include <optional> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/test/bind.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_future.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "chrome/browser/ash/file_manager/io_task.h" |
| #include "chrome/browser/ash/policy/skyvault/local_files_migration_constants.h" |
| #include "chrome/browser/ash/policy/skyvault/policy_utils.h" |
| #include "chrome/browser/ash/policy/skyvault/test/skyvault_test_base.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chromeos/ash/components/drivefs/mojom/drivefs.mojom.h" |
| #include "components/drive/file_errors.h" |
| #include "content/public/test/browser_test.h" |
| #include "storage/browser/file_system/file_system_url.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| |
| using storage::FileSystemURL; |
| |
| namespace policy::local_user_files { |
| |
| using ::base::test::RunOnceCallback; |
| using testing::_; |
| |
| // Tests the Drive SkyVault upload workflow. |
| class DriveSkyvaultUploaderTest : public SkyvaultGoogleDriveTest { |
| public: |
| DriveSkyvaultUploaderTest() = default; |
| |
| DriveSkyvaultUploaderTest(const DriveSkyvaultUploaderTest&) = delete; |
| DriveSkyvaultUploaderTest& operator=(const DriveSkyvaultUploaderTest&) = |
| delete; |
| |
| // `Wait` will not complete until this is called. |
| void OnUploadDone(std::optional<MigrationUploadError> error) { |
| if (fail_sync_) { |
| ASSERT_EQ(error, MigrationUploadError::kSyncFailed); |
| } else { |
| ASSERT_FALSE(error.has_value()); |
| } |
| EndWait(); |
| } |
| |
| base::FilePath observed_relative_drive_path(const FileInfo& info) override { |
| base::FilePath observed_relative_drive_path; |
| drive_integration_service()->GetRelativeDrivePath( |
| drive_root_dir() |
| .Append(kUploadRootPrefix) |
| .Append(info.local_relative_path_), |
| &observed_relative_drive_path); |
| return observed_relative_drive_path; |
| } |
| |
| protected: |
| base::HistogramTester histogram_tester_; |
| bool add_metadata_ = true; |
| bool fail_sync_ = false; |
| // Overrides `fail_sync_` |
| base::RepeatingClosure on_transfer_complete_callback_; |
| // Called when the copy task is starting/ongoing |
| base::RepeatingClosure on_copy_in_progress_callback_; |
| |
| private: |
| // IOTaskController::Observer: |
| void OnIOTaskStatus( |
| const file_manager::io_task::ProgressStatus& status) override { |
| auto it = source_files_.find(status.sources[0].url.path()); |
| if (status.type != file_manager::io_task::OperationType::kCopy || |
| status.sources.size() != 1 || it == source_files_.end()) { |
| return; |
| } |
| |
| // Invoke in progress callback if needed. |
| if (status.state == file_manager::io_task::State::kQueued || |
| status.state == file_manager::io_task::State::kInProgress) { |
| if (on_copy_in_progress_callback_) { |
| on_copy_in_progress_callback_.Run(); |
| // The callback might stop the IOTask, which would invalidate the status |
| // reference. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, std::move(on_copy_in_progress_callback_)); |
| return; |
| } |
| } |
| // Wait for the copy task to complete before starting the Drive sync. |
| if (status.state == file_manager::io_task::State::kSuccess) { |
| if (on_transfer_complete_callback_) { |
| on_transfer_complete_callback_.Run(); |
| } else if (fail_sync_) { |
| SimulateDriveUploadFailure(it->second); |
| } else { |
| SimulateDriveUploadCompleted(it->second); |
| } |
| } |
| } |
| |
| // Simulates the upload of the file to Drive by sending a series of fake |
| // signals to the DriveFs delegate. |
| void SimulateDriveUploadCompleted(const FileInfo& info) { |
| if (add_metadata_) { |
| // Set file metadata for `drivefs::mojom::DriveFs::GetMetadata`. |
| drivefs::FakeMetadata metadata; |
| metadata.path = observed_relative_drive_path(info); |
| metadata.mime_type = |
| "application/" |
| "vnd.openxmlformats-officedocument.wordprocessingml.document"; |
| metadata.original_name = info.test_file_name_; |
| metadata.alternate_url = |
| "https://docs.google.com/document/d/" |
| "smalldocxid?rtpof=true&usp=drive_fs"; |
| fake_drivefs().SetMetadata(std::move(metadata)); |
| } |
| // Simulate server sync events. |
| drivefs::mojom::SyncingStatusPtr status = |
| drivefs::mojom::SyncingStatus::New(); |
| status->item_events.emplace_back( |
| std::in_place, 12, 34, observed_relative_drive_path(info).value(), |
| drivefs::mojom::ItemEvent::State::kQueued, 123, 456, |
| drivefs::mojom::ItemEventReason::kTransfer); |
| drivefs_delegate()->OnSyncingStatusUpdate(status.Clone()); |
| drivefs_delegate().FlushForTesting(); |
| |
| status = drivefs::mojom::SyncingStatus::New(); |
| status->item_events.emplace_back( |
| std::in_place, 12, 34, observed_relative_drive_path(info).value(), |
| drivefs::mojom::ItemEvent::State::kCompleted, 123, 456, |
| drivefs::mojom::ItemEventReason::kTransfer); |
| drivefs_delegate()->OnSyncingStatusUpdate(status.Clone()); |
| drivefs_delegate().FlushForTesting(); |
| } |
| |
| void SimulateDriveUploadFailure(const FileInfo& info) { |
| // Simulate server sync events. |
| drivefs::mojom::SyncingStatusPtr status = |
| drivefs::mojom::SyncingStatus::New(); |
| status->item_events.emplace_back( |
| std::in_place, 12, 34, observed_relative_drive_path(info).value(), |
| drivefs::mojom::ItemEvent::State::kQueued, 123, 456, |
| drivefs::mojom::ItemEventReason::kTransfer); |
| drivefs_delegate()->OnSyncingStatusUpdate(status.Clone()); |
| drivefs_delegate().FlushForTesting(); |
| |
| drivefs::mojom::SyncingStatusPtr fail_status = |
| drivefs::mojom::SyncingStatus::New(); |
| fail_status->item_events.emplace_back( |
| std::in_place, 12, 34, observed_relative_drive_path(info).value(), |
| drivefs::mojom::ItemEvent::State::kFailed, 123, 456, |
| drivefs::mojom::ItemEventReason::kTransfer); |
| drivefs_delegate()->OnSyncingStatusUpdate(fail_status->Clone()); |
| drivefs_delegate().FlushForTesting(); |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(DriveSkyvaultUploaderTest, SuccessfulUpload) { |
| SetUpObservers(); |
| SetUpMyFiles(); |
| |
| const std::string test_file_name = "text.docx"; |
| const std::string test_dir_name = "foo"; |
| const base::FilePath dir_path = CreateTestDir(test_dir_name, my_files_dir()); |
| const base::FilePath source_file = |
| SetUpSourceFile(test_file_name, my_files_dir()); |
| |
| EXPECT_CALL(fake_drivefs(), ImmediatelyUpload) |
| .WillOnce(RunOnceCallback<1>(drive::FileError::FILE_ERROR_OK)); |
| |
| base::test::TestFuture<std::optional<MigrationUploadError>, base::FilePath> |
| future; |
| auto drive_upload_handler = std::make_unique<DriveSkyvaultUploader>( |
| profile(), source_file, base::FilePath(), kUploadRootPrefix, |
| future.GetCallback()); |
| drive_upload_handler->Run(); |
| |
| auto [error, upload_root_path] = future.Get(); |
| ASSERT_FALSE(error.has_value()); |
| EXPECT_EQ(drive_root_dir().Append(kUploadRootPrefix), upload_root_path); |
| |
| // Check that the source file has been moved to Drive. |
| { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| EXPECT_FALSE(base::PathExists(source_file)); |
| CheckPathExistsOnDrive( |
| observed_relative_drive_path(source_files_.find(source_file)->second)); |
| } |
| |
| histogram_tester_.ExpectBucketCount( |
| "Enterprise.SkyVault.Migration.GoogleDrive.DeleteError", false, 1); |
| histogram_tester_.ExpectBucketCount( |
| "Enterprise.SkyVault.Migration.GoogleDrive.DeleteError", true, 0); |
| } |
| |
| // Test that when the sync to Drive fails, the file is not moved to Drive. |
| IN_PROC_BROWSER_TEST_F(DriveSkyvaultUploaderTest, FailedUpload) { |
| fail_sync_ = true; |
| SetUpObservers(); |
| SetUpMyFiles(); |
| |
| const std::string test_file_name = "text.docx"; |
| const base::FilePath source_file = |
| SetUpSourceFile(test_file_name, my_files_dir()); |
| |
| EXPECT_CALL(fake_drivefs(), ImmediatelyUpload) |
| .WillOnce(RunOnceCallback<1>(drive::FileError::FILE_ERROR_FAILED)); |
| |
| base::test::TestFuture<std::optional<MigrationUploadError>, base::FilePath> |
| future; |
| auto drive_upload_handler = std::make_unique<DriveSkyvaultUploader>( |
| profile(), source_file, base::FilePath(), kUploadRootPrefix, |
| future.GetCallback()); |
| drive_upload_handler->Run(); |
| |
| auto [error, upload_root_path] = future.Get(); |
| ASSERT_EQ(MigrationUploadError::kSyncFailed, error); |
| EXPECT_EQ(drive_root_dir().Append(kUploadRootPrefix), upload_root_path); |
| |
| // Check that the source file has not been moved to Drive. |
| { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| EXPECT_TRUE(base::PathExists(my_files_dir().AppendASCII(test_file_name))); |
| CheckPathNotFoundOnDrive( |
| observed_relative_drive_path(source_files_.find(source_file)->second)); |
| } |
| |
| histogram_tester_.ExpectBucketCount( |
| "Enterprise.SkyVault.Migration.GoogleDrive.DeleteError", false, 1); |
| histogram_tester_.ExpectBucketCount( |
| "Enterprise.SkyVault.Migration.GoogleDrive.DeleteError", true, 0); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DriveSkyvaultUploaderTest, FailedDelete) { |
| SetUpObservers(); |
| SetUpMyFiles(); |
| |
| const std::string test_file_name = "text.docx"; |
| const base::FilePath source_file = |
| SetUpSourceFile(test_file_name, my_files_dir()); |
| |
| EXPECT_CALL(fake_drivefs(), ImmediatelyUpload) |
| .WillOnce(RunOnceCallback<1>(drive::FileError::FILE_ERROR_OK)); |
| |
| base::test::TestFuture<std::optional<MigrationUploadError>, base::FilePath> |
| future; |
| auto drive_upload_handler = std::make_unique<DriveSkyvaultUploader>( |
| profile(), source_file, base::FilePath(), kUploadRootPrefix, |
| future.GetCallback()); |
| drive_upload_handler->SetFailDeleteForTesting(/*fail=*/true); |
| drive_upload_handler->Run(); |
| |
| auto [error, upload_root_path] = future.Get(); |
| ASSERT_EQ(MigrationUploadError::kDeleteFailed, error); |
| EXPECT_EQ(drive_root_dir().Append(kUploadRootPrefix), upload_root_path); |
| |
| // Check that the source file has been moved to Drive. |
| { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| EXPECT_FALSE(base::PathExists(my_files_dir().AppendASCII(test_file_name))); |
| CheckPathExistsOnDrive( |
| observed_relative_drive_path(source_files_.find(source_file)->second)); |
| } |
| |
| histogram_tester_.ExpectBucketCount( |
| "Enterprise.SkyVault.Migration.GoogleDrive.DeleteError", false, 0); |
| histogram_tester_.ExpectBucketCount( |
| "Enterprise.SkyVault.Migration.GoogleDrive.DeleteError", true, 1); |
| } |
| |
| // Test that when connection to Drive isn't available, the upload fails |
| // immediately. |
| IN_PROC_BROWSER_TEST_F(DriveSkyvaultUploaderTest, NoConnection) { |
| SetUpObservers(); |
| SetUpMyFiles(); |
| SetDriveConnectionStatusForTesting(ConnectionStatus::kNoNetwork); |
| |
| const std::string test_file_name = "text.docx"; |
| const base::FilePath source_file = |
| SetUpSourceFile(test_file_name, my_files_dir()); |
| |
| EXPECT_CALL(fake_drivefs(), ImmediatelyUpload).Times(0); |
| |
| base::test::TestFuture<std::optional<MigrationUploadError>, base::FilePath> |
| future; |
| auto drive_upload_handler = std::make_unique<DriveSkyvaultUploader>( |
| profile(), source_file, base::FilePath(), kUploadRootPrefix, |
| future.GetCallback()); |
| drive_upload_handler->Run(); |
| |
| auto [error, upload_root_path] = future.Get(); |
| ASSERT_EQ(MigrationUploadError::kServiceUnavailable, error); |
| EXPECT_EQ(base::FilePath(), upload_root_path); |
| |
| // Check that the source file has not been moved to Drive. |
| { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| EXPECT_TRUE(base::PathExists(my_files_dir().AppendASCII(test_file_name))); |
| CheckPathNotFoundOnDrive( |
| observed_relative_drive_path(source_files_.find(source_file)->second)); |
| } |
| } |
| |
| // Test that when connection to Drive fails during upload, the file is not |
| // moved to Drive. |
| IN_PROC_BROWSER_TEST_F(DriveSkyvaultUploaderTest, ConnectionLostDuringUpload) { |
| SetUpObservers(); |
| SetUpMyFiles(); |
| |
| const std::string test_file_name = "text.docx"; |
| const base::FilePath source_file = |
| SetUpSourceFile(test_file_name, my_files_dir()); |
| |
| on_transfer_complete_callback_ = base::BindLambdaForTesting([this] { |
| SetDriveConnectionStatusForTesting(ConnectionStatus::kNoNetwork); |
| drive_integration_service()->OnNetworkChanged(); |
| }); |
| |
| base::test::TestFuture<std::optional<MigrationUploadError>, base::FilePath> |
| future; |
| auto drive_upload_handler = std::make_unique<DriveSkyvaultUploader>( |
| profile(), source_file, base::FilePath(), kUploadRootPrefix, |
| future.GetCallback()); |
| drive_upload_handler->Run(); |
| |
| auto [error, upload_root_path] = future.Get(); |
| ASSERT_EQ(MigrationUploadError::kServiceUnavailable, error); |
| EXPECT_EQ(drive_root_dir().Append(kUploadRootPrefix), upload_root_path); |
| |
| // Check that the source file has not been moved to Drive. |
| { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| EXPECT_TRUE(base::PathExists(my_files_dir().AppendASCII(test_file_name))); |
| CheckPathNotFoundOnDrive( |
| observed_relative_drive_path(source_files_.find(source_file)->second)); |
| } |
| } |
| |
| // Test that the upload can be cancelled after Run. |
| IN_PROC_BROWSER_TEST_F(DriveSkyvaultUploaderTest, Cancel) { |
| SetUpObservers(); |
| SetUpMyFiles(); |
| |
| const std::string test_file_name = "text.docx"; |
| const base::FilePath source_file = |
| SetUpSourceFile(test_file_name, my_files_dir()); |
| |
| base::test::TestFuture<std::optional<MigrationUploadError>, base::FilePath> |
| future; |
| auto drive_upload_handler = std::make_unique<DriveSkyvaultUploader>( |
| profile(), source_file, base::FilePath(), kUploadRootPrefix, |
| future.GetCallback()); |
| drive_upload_handler->Run(); |
| drive_upload_handler->Cancel(); |
| |
| auto [error, upload_root_path] = future.Get(); |
| ASSERT_EQ(MigrationUploadError::kCancelled, error); |
| EXPECT_EQ(drive_root_dir().Append(kUploadRootPrefix), upload_root_path); |
| |
| // Check that the source file has not been moved to Drive. |
| { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| EXPECT_TRUE(base::PathExists(my_files_dir().AppendASCII(test_file_name))); |
| CheckPathNotFoundOnDrive( |
| observed_relative_drive_path(source_files_.find(source_file)->second)); |
| } |
| } |
| |
| // Test that the upload can be cancelled after the copy task already started. |
| IN_PROC_BROWSER_TEST_F(DriveSkyvaultUploaderTest, CancelAfterCopyStarts) { |
| SetUpObservers(); |
| SetUpMyFiles(); |
| |
| const std::string test_file_name = "text.docx"; |
| const base::FilePath source_file = |
| SetUpSourceFile(test_file_name, my_files_dir()); |
| |
| base::test::TestFuture<std::optional<MigrationUploadError>, base::FilePath> |
| future; |
| auto drive_upload_handler = std::make_unique<DriveSkyvaultUploader>( |
| profile(), source_file, base::FilePath(), kUploadRootPrefix, |
| future.GetCallback()); |
| |
| on_copy_in_progress_callback_ = base::BindLambdaForTesting( |
| [&drive_upload_handler] { drive_upload_handler->Cancel(); }); |
| |
| drive_upload_handler->Run(); |
| |
| auto [error, upload_root_path] = future.Get(); |
| ASSERT_EQ(MigrationUploadError::kCancelled, error); |
| EXPECT_EQ(drive_root_dir().Append(kUploadRootPrefix), upload_root_path); |
| |
| // Check that the source file has not been deleted. |
| { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| EXPECT_TRUE(base::PathExists(my_files_dir().AppendASCII(test_file_name))); |
| CheckPathNotFoundOnDrive( |
| observed_relative_drive_path(source_files_.find(source_file)->second)); |
| } |
| } |
| |
| } // namespace policy::local_user_files |