chromium/chrome/browser/ash/fileapi/file_change_service_unittest.cc

// Copyright 2020 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/fileapi/file_change_service.h"

#include "base/files/scoped_temp_dir.h"
#include "base/memory/raw_ptr.h"
#include "base/scoped_observation.h"
#include "base/test/bind.h"
#include "base/unguessable_token.h"
#include "chrome/browser/ash/file_manager/app_id.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/fileapi/file_change_service_factory.h"
#include "chrome/browser/ash/fileapi/file_change_service_observer.h"
#include "chrome/browser/ash/fileapi/file_system_backend.h"
#include "chrome/browser/file_system_access/chrome_file_system_access_permission_context.h"
#include "chrome/browser/file_system_access/file_system_access_permission_context_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "mojo/public/cpp/system/data_pipe_producer.h"
#include "mojo/public/cpp/system/string_data_source.h"
#include "storage/browser/blob/blob_storage_context.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/browser/file_system/file_system_operation_runner.h"
#include "storage/browser/file_system/file_system_url.h"
#include "storage/browser/test/async_file_test_helper.h"
#include "storage/browser/test/mock_blob_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace ash {

namespace {

// Helpers ---------------------------------------------------------------------

// Returns the file system context associated with the specified `profile`.
storage::FileSystemContext* GetFileSystemContext(Profile* profile) {
  return file_manager::util::GetFileManagerFileSystemContext(profile);
}

// Creates a mojo data pipe with the provided `content`.
mojo::ScopedDataPipeConsumerHandle CreateStream(const std::string& contents) {
  mojo::ScopedDataPipeProducerHandle producer_handle;
  mojo::ScopedDataPipeConsumerHandle consumer_handle;
  MojoCreateDataPipeOptions options;
  options.struct_size = sizeof(MojoCreateDataPipeOptions);
  options.flags = MOJO_CREATE_DATA_PIPE_FLAG_NONE;
  options.element_num_bytes = 1;
  options.capacity_num_bytes = 16;
  mojo::CreateDataPipe(&options, producer_handle, consumer_handle);
  CHECK(producer_handle.is_valid());
  auto producer =
      std::make_unique<mojo::DataPipeProducer>(std::move(producer_handle));
  auto* producer_raw = producer.get();
  producer_raw->Write(
      std::make_unique<mojo::StringDataSource>(
          contents, mojo::StringDataSource::AsyncWritingMode::
                        STRING_MAY_BE_INVALIDATED_BEFORE_COMPLETION),
      base::BindOnce([](std::unique_ptr<mojo::DataPipeProducer>, MojoResult) {},
                     std::move(producer)));
  return consumer_handle;
}

// MockFileChangeServiceObserver -----------------------------------------------

class MockFileChangeServiceObserver : public FileChangeServiceObserver {
 public:
  // FileChangeServiceObserver:
  MOCK_METHOD(void,
              OnFileModified,
              (const storage::FileSystemURL& url),
              (override));
  MOCK_METHOD(void,
              OnFileMoved,
              (const storage::FileSystemURL& src,
               const storage::FileSystemURL& dst),
              (override));
  MOCK_METHOD(void,
              OnFileCreatedFromShowSaveFilePicker,
              (const GURL& file_picker_binding_context,
               const storage::FileSystemURL& url),
              (override));
};

// TempFileSystem --------------------------------------------------------------

// A class which registers a temporary file system and provides convenient APIs
// for interacting with that file system.
class TempFileSystem {
 public:
  explicit TempFileSystem(Profile* profile)
      : profile_(profile), name_(base::UnguessableToken::Create().ToString()) {}

  TempFileSystem(const TempFileSystem&) = delete;
  TempFileSystem& operator=(const TempFileSystem&) = delete;

  ~TempFileSystem() {
    storage::ExternalMountPoints::GetSystemInstance()->RevokeFileSystem(name_);
  }

  // Sets up and registers a temporary file system at `temp_dir_`.
  void SetUp() {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());

    ASSERT_TRUE(
        storage::ExternalMountPoints::GetSystemInstance()->RegisterFileSystem(
            name_, storage::kFileSystemTypeLocal,
            storage::FileSystemMountOption(), temp_dir_.GetPath()));

    ash::FileSystemBackend::Get(*GetFileSystemContext(profile_))
        ->GrantFileAccessToOrigin(file_manager::util::GetFilesAppOrigin(),
                                  base::FilePath(name_));
  }

  // Synchronously creates the file specified by `url`.
  base::File::Error CreateFile(const storage::FileSystemURL& url) {
    storage::FileSystemContext* context = GetFileSystemContext(profile_);
    return storage::AsyncFileTestHelper::CreateFile(context, url);
  }

  // Returns a file system URL for the specified path relative to `temp_dir_`.
  storage::FileSystemURL CreateFileSystemURL(const std::string& path) {
    return GetFileSystemContext(profile_)->CreateCrackedFileSystemURL(
        blink::StorageKey::CreateFirstParty(origin_),
        storage::kFileSystemTypeLocal,
        temp_dir_.GetPath().Append(base::FilePath::FromUTF8Unsafe(path)));
  }

  // Synchronously writes `content` to the file specified by `url`.
  base::File::Error WriteFile(const storage::FileSystemURL& url,
                              const std::string& data) {
    storage::BlobStorageContext blob_storage_context;
    storage::ScopedTextBlob blob(&blob_storage_context, "blob-id:test", data);
    base::File::Error result = base::File::FILE_ERROR_FAILED;
    base::RunLoop run_loop;
    GetFileSystemContext(profile_)->operation_runner()->Write(
        url, blob.GetBlobDataHandle(), 0,
        base::BindLambdaForTesting([&](base::File::Error operation_result,
                                       int64_t bytes, bool complete) {
          if (!complete)
            return;
          result = operation_result;
          run_loop.Quit();
        }));
    run_loop.Run();
    return result;
  }

  // Synchronously writes contents from `stream` to the file specified by `url`.
  base::File::Error WriteStreamToFile(
      const storage::FileSystemURL& url,
      mojo::ScopedDataPipeConsumerHandle stream) {
    base::File::Error result = base::File::FILE_ERROR_FAILED;
    base::RunLoop run_loop;
    GetFileSystemContext(profile_)->operation_runner()->WriteStream(
        url, std::move(stream), 0,
        base::BindLambdaForTesting([&](base::File::Error operation_result,
                                       int64_t bytes, bool complete) {
          if (!complete)
            return;
          result = operation_result;
          run_loop.Quit();
        }));
    run_loop.Run();
    return result;
  }

  // Synchronously truncates the file specified by `url` to `size`.
  base::File::Error TruncateFile(const storage::FileSystemURL& url,
                                 size_t size) {
    storage::FileSystemContext* context = GetFileSystemContext(profile_);
    return storage::AsyncFileTestHelper::TruncateFile(context, url, size);
  }

  // Synchronously copies the file specified by `src` to `dst`.
  base::File::Error CopyFile(const storage::FileSystemURL& src,
                             const storage::FileSystemURL& dst) {
    storage::FileSystemContext* context = GetFileSystemContext(profile_);
    return storage::AsyncFileTestHelper::Copy(context, src, dst);
  }

  // Synchronously copies the file specified by `src` to `dst` locally.
  base::File::Error CopyFileLocal(const storage::FileSystemURL& src,
                                  const storage::FileSystemURL& dst) {
    storage::FileSystemContext* context = GetFileSystemContext(profile_);
    return storage::AsyncFileTestHelper::CopyFileLocal(context, src, dst);
  }

  // Synchronously moves the file specified by `src` to `dst`.
  base::File::Error MoveFile(const storage::FileSystemURL& src,
                             const storage::FileSystemURL& dst) {
    storage::FileSystemContext* context = GetFileSystemContext(profile_);
    return storage::AsyncFileTestHelper::Move(context, src, dst);
  }

  // Synchronously moves the file specified by `src` to `dst` locally.
  base::File::Error MoveFileLocal(const storage::FileSystemURL& src,
                                  const storage::FileSystemURL& dst) {
    storage::FileSystemContext* context = GetFileSystemContext(profile_);
    return storage::AsyncFileTestHelper::MoveFileLocal(context, src, dst);
  }

  // Synchronously removes the file specified by `url`.
  base::File::Error RemoveFile(const storage::FileSystemURL& url,
                               bool recursive = false) {
    storage::FileSystemContext* context = GetFileSystemContext(profile_);
    return storage::AsyncFileTestHelper::Remove(context, url, recursive);
  }

 private:
  const raw_ptr<Profile> profile_;
  const url::Origin origin_;
  const std::string name_;
  base::ScopedTempDir temp_dir_;
};

// FileChangeServiceTest -------------------------------------------------------

class FileChangeServiceTest : public BrowserWithTestWindowTest {
 public:
  FileChangeServiceTest() = default;

  FileChangeServiceTest(const FileChangeServiceTest& other) = delete;
  FileChangeServiceTest& operator=(const FileChangeServiceTest& other) = delete;
  ~FileChangeServiceTest() override = default;

  // Creates and returns a new profile for the specified `name`.
  TestingProfile* CreateLoggedInUserProfile(const std::string& name) {
    LogIn(name);
    return CreateProfile(name);
  }

 private:
  // BrowserWithTestWindowTest:
  std::string GetDefaultProfileName() override {
    return "promary_profile@test";
  }
};

}  // namespace

// Tests -----------------------------------------------------------------------

// Verifies service instances are created on a per-profile basis.
TEST_F(FileChangeServiceTest, CreatesServiceInstancesPerProfile) {
  auto* factory = FileChangeServiceFactory::GetInstance();
  ASSERT_TRUE(factory);

  // `FileChangeService` should exist for the primary profile.
  auto* primary_profile = GetProfile();
  auto* primary_profile_service = factory->GetService(primary_profile);
  ASSERT_TRUE(primary_profile_service);

  // `FileChangeService` should be created as needed for additional profiles.
  constexpr char kSecondaryProfileName[] = "secondary_profile@test";
  auto* secondary_profile = CreateLoggedInUserProfile(kSecondaryProfileName);
  auto* secondary_profile_service = factory->GetService(secondary_profile);
  ASSERT_TRUE(secondary_profile_service);

  // Per-profile services should be unique.
  ASSERT_NE(primary_profile_service, secondary_profile_service);
}

// Verifies service instances are *not* created for OTR profiles.
TEST_F(FileChangeServiceTest, DoesntCreateServiceInstanceForOTRProfile) {
  auto* factory = FileChangeServiceFactory::GetInstance();
  ASSERT_TRUE(factory);

  // `FileChangeService` should be created for non-OTR profile.
  auto* profile = GetProfile();
  ASSERT_TRUE(profile);
  ASSERT_FALSE(profile->IsOffTheRecord());
  ASSERT_TRUE(factory->GetService(profile));

  // `FileChangeService` should *not* be created for OTR profile.
  auto* otr_profile =
      TestingProfile::Builder().BuildIncognito(profile->AsTestingProfile());
  ASSERT_TRUE(otr_profile);
  ASSERT_TRUE(otr_profile->IsOffTheRecord());
  ASSERT_FALSE(factory->GetService(otr_profile));
}

// Verifies service instance *are* created for guest OTR profiles.
TEST_F(FileChangeServiceTest, CreatesServiceInstanceForOTRGuestProfile) {
  auto* factory = FileChangeServiceFactory::GetInstance();
  ASSERT_TRUE(factory);

  // Construct a guest profile.
  TestingProfile::Builder guest_profile_builder;
  guest_profile_builder.SetGuestSession();
  guest_profile_builder.SetProfileName("guest_profile");
  std::unique_ptr<TestingProfile> guest_profile = guest_profile_builder.Build();

  // Service instances should be created for guest profiles.
  ASSERT_TRUE(guest_profile);
  ASSERT_FALSE(guest_profile->IsOffTheRecord());
  FileChangeService* const guest_profile_service =
      factory->GetService(guest_profile.get());
  ASSERT_TRUE(guest_profile_service);

  // Construct an OTR profile from `guest_profile`.
  TestingProfile::Builder otr_guest_profile_builder;
  otr_guest_profile_builder.SetGuestSession();
  otr_guest_profile_builder.SetProfileName(guest_profile->GetProfileUserName());
  Profile* const otr_guest_profile =
      otr_guest_profile_builder.BuildIncognito(guest_profile.get());
  ASSERT_TRUE(otr_guest_profile);
  ASSERT_TRUE(otr_guest_profile->IsOffTheRecord());

  // Service instances *should* be created for OTR guest profiles.
  FileChangeService* const otr_guest_profile_service =
      factory->GetService(otr_guest_profile);
  ASSERT_TRUE(otr_guest_profile_service);

  // OTR service instances should be distinct from non-OTR service instances.
  ASSERT_NE(otr_guest_profile_service, guest_profile_service);
}

// Verifies `OnFileMoved()` events are propagated to observers.
TEST_F(FileChangeServiceTest, PropagatesOnFileMovedEvents) {
  auto* profile = GetProfile();
  auto* service = FileChangeServiceFactory::GetInstance()->GetService(profile);
  ASSERT_TRUE(service);

  testing::NiceMock<MockFileChangeServiceObserver> mock_observer;
  base::ScopedObservation<FileChangeService, FileChangeServiceObserver>
      scoped_observation{&mock_observer};
  scoped_observation.Observe(service);

  TempFileSystem temp_file_system(profile);
  temp_file_system.SetUp();

  storage::FileSystemURL src = temp_file_system.CreateFileSystemURL("src");
  storage::FileSystemURL dst = temp_file_system.CreateFileSystemURL("dst");

  ASSERT_EQ(temp_file_system.CreateFile(src), base::File::FILE_OK);

  {
    base::RunLoop move_run_loop;
    EXPECT_CALL(mock_observer, OnFileMoved)
        // NOTE: `Move()` internally calls `MoveFileLocal()`, so move operation
        // gets reported twice.
        .WillOnce([&](const storage::FileSystemURL& propagated_src,
                      const storage::FileSystemURL& propagated_dst) {
          EXPECT_EQ(src, propagated_src);
          EXPECT_EQ(dst, propagated_dst);
        })
        .WillOnce([&](const storage::FileSystemURL& propagated_src,
                      const storage::FileSystemURL& propagated_dst) {
          EXPECT_EQ(src, propagated_src);
          EXPECT_EQ(dst, propagated_dst);
          move_run_loop.Quit();
        })
        .RetiresOnSaturation();

    EXPECT_CALL(mock_observer, OnFileModified).Times(0);

    ASSERT_EQ(temp_file_system.MoveFile(src, dst), base::File::FILE_OK);
    move_run_loop.Run();
  }

  ::testing::Mock::VerifyAndClearExpectations(&mock_observer);

  {
    base::RunLoop move_run_loop;
    EXPECT_CALL(mock_observer, OnFileMoved)
        .WillOnce([&](const storage::FileSystemURL& propagated_src,
                      const storage::FileSystemURL& propagated_dst) {
          EXPECT_EQ(dst, propagated_src);
          EXPECT_EQ(src, propagated_dst);
          move_run_loop.Quit();
        })
        .RetiresOnSaturation();

    EXPECT_CALL(mock_observer, OnFileModified).Times(0);
    ASSERT_EQ(temp_file_system.MoveFileLocal(dst, src), base::File::FILE_OK);

    move_run_loop.Run();
  }
}

// Verifies `OnFileModified()` events are propagated to observers.
TEST_F(FileChangeServiceTest, PropagatesOnFileModifiedEvents) {
  auto* profile = GetProfile();
  auto* service = FileChangeServiceFactory::GetInstance()->GetService(profile);
  ASSERT_TRUE(service);

  testing::NiceMock<MockFileChangeServiceObserver> mock_observer;
  base::ScopedObservation<FileChangeService, FileChangeServiceObserver>
      scoped_observation{&mock_observer};
  scoped_observation.Observe(service);

  TempFileSystem temp_file_system(profile);
  temp_file_system.SetUp();

  storage::FileSystemURL url =
      temp_file_system.CreateFileSystemURL("test_file");

  ASSERT_EQ(temp_file_system.CreateFile(url), base::File::FILE_OK);

  // Test writing to file.
  {
    base::RunLoop modify_run_loop;
    EXPECT_CALL(mock_observer, OnFileModified)
        .WillOnce([&](const storage::FileSystemURL& propagated_url) {
          EXPECT_EQ(url, propagated_url);
          modify_run_loop.Quit();
        })
        .RetiresOnSaturation();

    ASSERT_EQ(temp_file_system.WriteFile(url, "Test file contents\n"),
              base::File::FILE_OK);
    modify_run_loop.Run();
  }

  ::testing::Mock::VerifyAndClearExpectations(&mock_observer);

  // Test truncating file.
  {
    base::RunLoop modify_run_loop;
    EXPECT_CALL(mock_observer, OnFileModified)
        .WillOnce([&](const storage::FileSystemURL& propagated_url) {
          EXPECT_EQ(url, propagated_url);
          modify_run_loop.Quit();
        })
        .RetiresOnSaturation();

    ASSERT_EQ(temp_file_system.TruncateFile(url, 10), base::File::FILE_OK);
    modify_run_loop.Run();
  }

  // Test writing a stream to file.
  {
    base::RunLoop modify_run_loop;
    EXPECT_CALL(mock_observer, OnFileModified)
        .WillOnce([&](const storage::FileSystemURL& propagated_url) {
          EXPECT_EQ(url, propagated_url);
          modify_run_loop.Quit();
        })
        .RetiresOnSaturation();

    ASSERT_EQ(temp_file_system.WriteStreamToFile(
                  url, CreateStream("Test file contents from stream")),
              base::File::FILE_OK);
    modify_run_loop.Run();
  }
}

// Verifies `OnFileCreatedFromShowSaveFilePicker()` events are propagated to
// observers.
TEST_F(FileChangeServiceTest,
       PropagatesOnFileCreatedFromShowSaveFilePickerEvents) {
  auto* profile = GetProfile();
  auto* service = FileChangeServiceFactory::GetInstance()->GetService(profile);
  ASSERT_TRUE(service);

  testing::NiceMock<MockFileChangeServiceObserver> mock_observer;
  base::ScopedObservation<FileChangeService, FileChangeServiceObserver>
      scoped_observation(&mock_observer);
  scoped_observation.Observe(service);

  const GURL file_picker_binding_context;
  const storage::FileSystemURL url;

  EXPECT_CALL(mock_observer, OnFileCreatedFromShowSaveFilePicker(
                                 testing::Ref(file_picker_binding_context),
                                 testing::Ref(url)));

  FileSystemAccessPermissionContextFactory::GetForProfile(profile)
      ->OnFileCreatedFromShowSaveFilePicker(file_picker_binding_context, url);
}

}  // namespace ash