chromium/chrome/browser/ash/file_system_provider/content_cache/local_fd_unittest.cc

// 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/file_system_provider/content_cache/local_fd.h"

#include <cstring>

#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/scoped_refptr.h"
#include "base/rand_util.h"
#include "base/task/thread_pool.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "net/base/io_buffer.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash::file_system_provider {
namespace {

using testing::Property;

class FileSystemProviderLocalFDTest : public testing::Test {
 protected:
  FileSystemProviderLocalFDTest() = default;
  ~FileSystemProviderLocalFDTest() override = default;

  void SetUp() override {
    EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
    task_runner_ = base::ThreadPool::CreateSequencedTaskRunner(
        {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
         base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN});

    // Create a test file that will be used to read and write in every test.
    test_file_path_ = base::FilePath(temp_dir_.GetPath().Append("test"));
    test_file_contents_ = "test data";
    test_file_size_ = test_file_contents_.size();
    EXPECT_TRUE(base::WriteFile(test_file_path_, test_file_contents_));
  }

  base::FilePath test_file_path_;
  int test_file_size_ = -1;
  std::string test_file_contents_;
  scoped_refptr<base::SequencedTaskRunner> task_runner_;

  base::test::TaskEnvironment task_environment_;
  base::ScopedTempDir temp_dir_;
};

TEST_F(FileSystemProviderLocalFDTest, BytesCanBeReadSuccessfully) {
  // Create a buffer that the contents of `file_path` will be read into.
  scoped_refptr<net::IOBuffer> read_buffer =
      base::MakeRefCounted<net::IOBufferWithSize>(test_file_size_);

  LocalFD file(test_file_path_, task_runner_);
  base::test::TestFuture<base::FileErrorOr<int>> read_bytes_future;
  file.ReadBytes(read_buffer, /*offset=*/0, /*length=*/test_file_size_,
                 read_bytes_future.GetCallback());

  // Ensure that both the `bytes_read` on the callback and the actual size of
  // the buffer are the size of the data written to the underlying file.
  EXPECT_EQ(read_bytes_future.Get(), test_file_size_);
  EXPECT_EQ(read_buffer->size(), test_file_size_);

  // Close the file.
  base::test::TestFuture<void> close_future;
  file.Close(close_future.GetCallback());
  EXPECT_TRUE(close_future.Wait());
}

TEST_F(FileSystemProviderLocalFDTest, BytesCanBeReadThenWrittenTo) {
  scoped_refptr<net::IOBuffer> read_buffer =
      base::MakeRefCounted<net::IOBufferWithSize>(test_file_size_);

  // Read the current bytes from the existing file.
  LocalFD file(test_file_path_, task_runner_);
  base::test::TestFuture<base::FileErrorOr<int>> future;
  file.ReadBytes(read_buffer, /*offset=*/0, /*length=*/test_file_size_,
                 future.GetCallback());
  EXPECT_EQ(future.Get(), test_file_size_);
  EXPECT_EQ(read_buffer->size(), test_file_size_);

  // Prepare a buffer with some data to append to the existing file.
  std::string contents_to_append(" appended data");
  scoped_refptr<net::IOBuffer> write_buffer =
      base::MakeRefCounted<net::StringIOBuffer>(contents_to_append);

  // Attempt to write bytes to the existing file. The underlying `base::File`
  // was created with `base::File::FLAG_OPEN | base::File::FLAG_READ` which
  // ensures a failure if the file doesn't exist. Given we're not attempting to
  // write to the file, this should close that FD and re-open one that enables
  // writing
  base::test::TestFuture<base::File::Error> write_bytes_future;
  file.WriteBytes(write_buffer, test_file_size_, /*length=*/15,
                  write_bytes_future.GetCallback());
  EXPECT_EQ(write_bytes_future.Get(), base::File::FILE_OK);

  // Ensure the actual contents on disk get updated to include the appended
  // data.
  std::string actual_contents;
  EXPECT_TRUE(base::ReadFileToString(test_file_path_, &actual_contents));
  EXPECT_STREQ(actual_contents.c_str(),
               base::StrCat({test_file_contents_, contents_to_append}).c_str());
}

TEST_F(FileSystemProviderLocalFDTest, FileCanBeScheduledToBeClosed) {
  scoped_refptr<net::IOBuffer> buffer =
      base::MakeRefCounted<net::IOBufferWithSize>(test_file_size_);

  LocalFD file(test_file_path_, task_runner_);
  base::test::TestFuture<base::FileErrorOr<int>> read_bytes_future;
  file.ReadBytes(buffer, /*offset=*/0, /*length=*/test_file_size_,
                 read_bytes_future.GetCallback());

  // Expect file closure to eventually succeed.
  base::test::TestFuture<void> close_future;
  file.Close(close_future.GetCallback());
  EXPECT_EQ(read_bytes_future.Get(), test_file_size_);
  EXPECT_TRUE(close_future.Wait());
}

TEST_F(FileSystemProviderLocalFDTest, ConcurrentReadsAreNotAllowed) {
  scoped_refptr<net::IOBuffer> buffer =
      base::MakeRefCounted<net::IOBufferWithSize>(test_file_size_);

  LocalFD file(test_file_path_, task_runner_);

  // First read call should succeed.
  base::test::TestFuture<base::FileErrorOr<int>> read_bytes_future_allowed;
  file.ReadBytes(buffer, /*offset=*/0, /*length=*/1,
                 read_bytes_future_allowed.GetCallback());

  // Second call should not succeed as the first call is still waiting to be
  // executed.
  base::test::TestFuture<base::FileErrorOr<int>> read_bytes_future_disallowed;
  file.ReadBytes(buffer, /*offset=*/1, /*length=*/1,
                 read_bytes_future_disallowed.GetCallback());

  EXPECT_THAT(read_bytes_future_allowed.Get(),
              Property(&base::FileErrorOr<int>::value, 1));
  EXPECT_THAT(
      read_bytes_future_disallowed.Get(),
      Property(&base::FileErrorOr<int>::error, base::File::FILE_ERROR_IN_USE));

  // Expect file closure to eventually succeed.
  base::test::TestFuture<void> close_future;
  file.Close(close_future.GetCallback());
  EXPECT_TRUE(close_future.Wait());
}

}  // namespace
}  // namespace ash::file_system_provider