| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "remoting/host/file_transfer/ipc_file_operations.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/containers/queue.h" |
| #include "base/files/file_util.h" |
| #include "base/path_service.h" |
| #include "base/test/scoped_path_override.h" |
| #include "base/test/scoped_task_environment.h" |
| #include "remoting/host/file_transfer/fake_file_chooser.h" |
| #include "remoting/host/file_transfer/local_file_operations.h" |
| #include "remoting/host/file_transfer/session_file_operations_handler.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace remoting { |
| |
| namespace { |
| |
| // BindOnce disallows binding lambdas with captures. This is reasonable in |
| // production code, as it requires one to either explicitly pass owned objects |
| // or pointers using Owned, Unretained, et cetera. This helps to avoid use after |
| // free bugs in async code. In test code, though, where the lambda is |
| // immediately invoked in the test method using, e.g., RunUntilIdle, the ability |
| // to capture can make the code much easier to read and write. |
| template <typename T> |
| auto BindLambda(T lambda) { |
| return base::BindOnce(&T::operator(), |
| base::Owned(new auto(std::move(lambda)))); |
| } |
| |
| class IpcTestBridge : public IpcFileOperations::RequestHandler, |
| public IpcFileOperations::ResultHandler, |
| public FileOperations { |
| public: |
| explicit IpcTestBridge( |
| scoped_refptr<base::SequencedTaskRunner> ui_task_runner) |
| : ipc_file_operations_factory_(this), |
| session_file_operations_handler_( |
| this, |
| std::make_unique<LocalFileOperations>(std::move(ui_task_runner))), |
| file_operations_(ipc_file_operations_factory_.CreateFileOperations()) {} |
| |
| ~IpcTestBridge() override = default; |
| |
| // IpcFileOperations::RequestHandler implementation. |
| void ReadFile(std::uint64_t file_id) override { |
| session_file_operations_handler_.ReadFile(file_id); |
| } |
| void ReadChunk(std::uint64_t file_id, std::uint64_t size) override { |
| session_file_operations_handler_.ReadChunk(file_id, size); |
| } |
| void WriteFile(std::uint64_t file_id, |
| const base::FilePath& filename) override { |
| session_file_operations_handler_.WriteFile(file_id, filename); |
| } |
| void WriteChunk(std::uint64_t file_id, std::string data) override { |
| session_file_operations_handler_.WriteChunk(file_id, std::move(data)); |
| } |
| void Close(std::uint64_t file_id) override { |
| session_file_operations_handler_.Close(file_id); |
| } |
| void Cancel(std::uint64_t file_id) override { |
| session_file_operations_handler_.Cancel(file_id); |
| } |
| |
| // ResultHandler implementation. |
| void OnResult(std::uint64_t file_id, Result result) override { |
| ipc_file_operations_factory_.OnResult(file_id, std::move(result)); |
| } |
| void OnInfoResult(std::uint64_t file_id, InfoResult result) override { |
| ipc_file_operations_factory_.OnInfoResult(file_id, std::move(result)); |
| } |
| void OnDataResult(std::uint64_t file_id, DataResult result) override { |
| ipc_file_operations_factory_.OnDataResult(file_id, std::move(result)); |
| } |
| |
| // FileOperations implementation. |
| std::unique_ptr<Reader> CreateReader() override { |
| return file_operations_->CreateReader(); |
| } |
| std::unique_ptr<Writer> CreateWriter() override { |
| return file_operations_->CreateWriter(); |
| } |
| |
| private: |
| IpcFileOperationsFactory ipc_file_operations_factory_; |
| SessionFileOperationsHandler session_file_operations_handler_; |
| std::unique_ptr<FileOperations> file_operations_; |
| |
| DISALLOW_COPY_AND_ASSIGN(IpcTestBridge); |
| }; |
| |
| } // namespace |
| |
| class IpcFileOperationsTest : public testing::Test { |
| public: |
| IpcFileOperationsTest(); |
| ~IpcFileOperationsTest() override; |
| |
| protected: |
| const base::FilePath kTestFilename = |
| base::FilePath::FromUTF8Unsafe("test-file.txt"); |
| const std::string kTestDataOne = "this is the first test string"; |
| const std::string kTestDataTwo = "this is the second test string"; |
| const std::string kTestDataThree = "this is the third test string"; |
| |
| base::FilePath TestDir(); |
| |
| base::test::ScopedTaskEnvironment scoped_task_environment_; |
| base::ScopedPathOverride scoped_path_override_; |
| std::unique_ptr<FileOperations> file_operations_; |
| |
| DISALLOW_COPY_AND_ASSIGN(IpcFileOperationsTest); |
| }; |
| |
| IpcFileOperationsTest::IpcFileOperationsTest() |
| : scoped_task_environment_( |
| base::test::ScopedTaskEnvironment::MainThreadType::DEFAULT, |
| base::test::ScopedTaskEnvironment::ExecutionMode::QUEUED), |
| // Points DIR_USER_DESKTOP at a scoped temporary directory. |
| scoped_path_override_(base::DIR_USER_DESKTOP), |
| file_operations_(std::make_unique<IpcTestBridge>( |
| scoped_task_environment_.GetMainThreadTaskRunner())) {} |
| |
| IpcFileOperationsTest::~IpcFileOperationsTest() = default; |
| |
| base::FilePath IpcFileOperationsTest::TestDir() { |
| base::FilePath result; |
| EXPECT_TRUE(base::PathService::Get(base::DIR_USER_DESKTOP, &result)); |
| return result; |
| } |
| |
| // Verifies that a file consisting of three chunks can be written successfully. |
| TEST_F(IpcFileOperationsTest, WritesThreeChunks) { |
| std::unique_ptr<FileOperations::Writer> writer = |
| file_operations_->CreateWriter(); |
| ASSERT_EQ(FileOperations::kCreated, writer->state()); |
| |
| base::Optional<FileOperations::Writer::Result> open_result; |
| writer->Open(kTestFilename, |
| BindLambda([&](FileOperations::Writer::Result result) { |
| open_result = std::move(result); |
| })); |
| ASSERT_EQ(FileOperations::kBusy, writer->state()); |
| scoped_task_environment_.RunUntilIdle(); |
| ASSERT_EQ(FileOperations::kReady, writer->state()); |
| ASSERT_TRUE(open_result); |
| ASSERT_TRUE(*open_result); |
| |
| for (const auto& chunk : {kTestDataOne, kTestDataTwo, kTestDataThree}) { |
| base::Optional<FileOperations::Writer::Result> write_result; |
| writer->WriteChunk(chunk, |
| BindLambda([&](FileOperations::Writer::Result result) { |
| write_result = std::move(result); |
| })); |
| ASSERT_EQ(FileOperations::kBusy, writer->state()); |
| scoped_task_environment_.RunUntilIdle(); |
| ASSERT_EQ(FileOperations::kReady, writer->state()); |
| ASSERT_TRUE(write_result); |
| ASSERT_TRUE(*write_result); |
| } |
| |
| base::Optional<FileOperations::Writer::Result> close_result; |
| writer->Close(BindLambda([&](FileOperations::Writer::Result result) { |
| close_result = std::move(result); |
| })); |
| ASSERT_EQ(FileOperations::kBusy, writer->state()); |
| scoped_task_environment_.RunUntilIdle(); |
| EXPECT_EQ(FileOperations::kComplete, writer->state()); |
| |
| std::string actual_file_data; |
| ASSERT_TRUE(base::ReadFileToString(TestDir().Append(kTestFilename), |
| &actual_file_data)); |
| EXPECT_EQ(kTestDataOne + kTestDataTwo + kTestDataThree, actual_file_data); |
| } |
| |
| // Verifies that dropping early cancels the remote writer. |
| TEST_F(IpcFileOperationsTest, DroppingCancelsRemote) { |
| std::unique_ptr<FileOperations::Writer> writer = |
| file_operations_->CreateWriter(); |
| |
| base::Optional<FileOperations::Writer::Result> open_result; |
| writer->Open(kTestFilename, |
| BindLambda([&](FileOperations::Writer::Result result) { |
| open_result = std::move(result); |
| })); |
| scoped_task_environment_.RunUntilIdle(); |
| ASSERT_TRUE(open_result && *open_result); |
| |
| for (const auto& chunk : {kTestDataOne, kTestDataTwo, kTestDataThree}) { |
| base::Optional<FileOperations::Writer::Result> write_result; |
| writer->WriteChunk(chunk, |
| BindLambda([&](FileOperations::Writer::Result result) { |
| write_result = std::move(result); |
| })); |
| scoped_task_environment_.RunUntilIdle(); |
| ASSERT_TRUE(write_result && *write_result); |
| } |
| |
| writer.reset(); |
| scoped_task_environment_.RunUntilIdle(); |
| |
| EXPECT_TRUE(base::IsDirectoryEmpty(TestDir())); |
| } |
| |
| // Verifies that dropping works while an operation is pending. |
| TEST_F(IpcFileOperationsTest, CancelsWhileOperationPending) { |
| std::unique_ptr<FileOperations::Writer> writer = |
| file_operations_->CreateWriter(); |
| |
| base::Optional<FileOperations::Writer::Result> open_result; |
| writer->Open(kTestFilename, |
| BindLambda([&](FileOperations::Writer::Result result) { |
| open_result = std::move(result); |
| })); |
| scoped_task_environment_.RunUntilIdle(); |
| ASSERT_TRUE(open_result && *open_result); |
| |
| base::Optional<FileOperations::Writer::Result> write_result; |
| writer->WriteChunk(std::string(kTestDataOne), |
| BindLambda([&](FileOperations::Writer::Result result) { |
| write_result = std::move(result); |
| })); |
| |
| EXPECT_EQ(FileOperations::kBusy, writer->state()); |
| writer.reset(); |
| scoped_task_environment_.RunUntilIdle(); |
| |
| EXPECT_FALSE(write_result); |
| EXPECT_TRUE(base::IsDirectoryEmpty(TestDir())); |
| } |
| |
| // Verifies that a file can be successfully read in three chunks. |
| TEST_F(IpcFileOperationsTest, ReadsThreeChunks) { |
| base::FilePath path = TestDir().Append(kTestFilename); |
| std::string contents = kTestDataOne + kTestDataTwo + kTestDataThree; |
| ASSERT_EQ(static_cast<int>(contents.size()), |
| base::WriteFile(path, contents.data(), contents.size())); |
| |
| std::unique_ptr<FileOperations::Reader> reader = |
| file_operations_->CreateReader(); |
| ASSERT_EQ(FileOperations::kCreated, reader->state()); |
| |
| FakeFileChooser::SetResult(path); |
| base::Optional<FileOperations::Reader::OpenResult> open_result; |
| reader->Open(BindLambda([&](FileOperations::Reader::OpenResult result) { |
| open_result = std::move(result); |
| })); |
| ASSERT_EQ(FileOperations::kBusy, reader->state()); |
| scoped_task_environment_.RunUntilIdle(); |
| EXPECT_EQ(FileOperations::kReady, reader->state()); |
| ASSERT_TRUE(open_result); |
| ASSERT_TRUE(*open_result); |
| |
| for (const auto& chunk : {kTestDataOne, kTestDataTwo, kTestDataThree}) { |
| base::Optional<FileOperations::Reader::ReadResult> read_result; |
| reader->ReadChunk( |
| chunk.size(), |
| BindLambda([&](FileOperations::Reader::ReadResult result) { |
| read_result = std::move(result); |
| })); |
| ASSERT_EQ(FileOperations::kBusy, reader->state()); |
| scoped_task_environment_.RunUntilIdle(); |
| ASSERT_EQ(FileOperations::kReady, reader->state()); |
| ASSERT_TRUE(read_result); |
| ASSERT_TRUE(*read_result); |
| EXPECT_EQ(chunk, **read_result); |
| } |
| } |
| |
| // Verifies proper EOF handling. |
| TEST_F(IpcFileOperationsTest, ReaderHandlesEof) { |
| constexpr std::size_t kOverreadAmount = 5; |
| base::FilePath path = TestDir().Append(kTestFilename); |
| std::string contents = kTestDataOne + kTestDataTwo + kTestDataThree; |
| ASSERT_EQ(static_cast<int>(contents.size()), |
| base::WriteFile(path, contents.data(), contents.size())); |
| |
| std::unique_ptr<FileOperations::Reader> reader = |
| file_operations_->CreateReader(); |
| |
| FakeFileChooser::SetResult(path); |
| base::Optional<FileOperations::Reader::OpenResult> open_result; |
| reader->Open(BindLambda([&](FileOperations::Reader::OpenResult result) { |
| open_result = std::move(result); |
| })); |
| scoped_task_environment_.RunUntilIdle(); |
| ASSERT_TRUE(open_result && *open_result); |
| |
| base::Optional<FileOperations::Reader::ReadResult> read_result; |
| reader->ReadChunk( |
| contents.size() + |
| kOverreadAmount, // Attempt to read more than is in file. |
| BindLambda([&](FileOperations::Reader::ReadResult result) { |
| read_result = std::move(result); |
| })); |
| scoped_task_environment_.RunUntilIdle(); |
| ASSERT_EQ(FileOperations::kReady, reader->state()); |
| ASSERT_TRUE(read_result); |
| ASSERT_TRUE(*read_result); |
| EXPECT_EQ(contents, **read_result); |
| |
| read_result.reset(); |
| reader->ReadChunk(kOverreadAmount, |
| BindLambda([&](FileOperations::Reader::ReadResult result) { |
| read_result = std::move(result); |
| })); |
| scoped_task_environment_.RunUntilIdle(); |
| EXPECT_EQ(FileOperations::kComplete, reader->state()); |
| ASSERT_TRUE(read_result); |
| ASSERT_TRUE(*read_result); |
| EXPECT_EQ(std::size_t{0}, (*read_result)->size()); |
| } |
| |
| // Verifies proper handling of zero-size file |
| TEST_F(IpcFileOperationsTest, ReaderHandlesZeroSize) { |
| constexpr std::size_t kChunkSize = 5; |
| base::FilePath path = TestDir().Append(kTestFilename); |
| ASSERT_EQ(0, base::WriteFile(path, "", 0)); |
| |
| std::unique_ptr<FileOperations::Reader> reader = |
| file_operations_->CreateReader(); |
| |
| FakeFileChooser::SetResult(path); |
| base::Optional<FileOperations::Reader::OpenResult> open_result; |
| reader->Open(BindLambda([&](FileOperations::Reader::OpenResult result) { |
| open_result = std::move(result); |
| })); |
| scoped_task_environment_.RunUntilIdle(); |
| ASSERT_TRUE(open_result && *open_result); |
| |
| base::Optional<FileOperations::Reader::ReadResult> read_result; |
| reader->ReadChunk(kChunkSize, |
| BindLambda([&](FileOperations::Reader::ReadResult result) { |
| read_result = std::move(result); |
| })); |
| scoped_task_environment_.RunUntilIdle(); |
| EXPECT_EQ(FileOperations::kComplete, reader->state()); |
| ASSERT_TRUE(read_result); |
| ASSERT_TRUE(*read_result); |
| EXPECT_EQ(std::size_t{0}, (*read_result)->size()); |
| } |
| |
| // Verifies error is propagated. |
| TEST_F(IpcFileOperationsTest, ReaderPropagatesError) { |
| std::unique_ptr<FileOperations::Reader> reader = |
| file_operations_->CreateReader(); |
| |
| // Currently non-existent file. |
| FakeFileChooser::SetResult(TestDir().Append(kTestFilename)); |
| base::Optional<FileOperations::Reader::OpenResult> open_result; |
| reader->Open(BindLambda([&](FileOperations::Reader::OpenResult result) { |
| open_result = std::move(result); |
| })); |
| scoped_task_environment_.RunUntilIdle(); |
| EXPECT_EQ(FileOperations::kFailed, reader->state()); |
| ASSERT_TRUE(open_result); |
| ASSERT_FALSE(*open_result); |
| } |
| |
| } // namespace remoting |