| // 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 <algorithm> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/files/file_util.h" |
| #include "base/path_service.h" |
| #include "base/task/thread_pool.h" |
| #include "base/test/task_environment.h" |
| #include "base/threading/sequenced_task_runner_handle.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "build/build_config.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/services/cups_proxy/fake_cups_proxy_service_delegate.h" |
| #include "chrome/services/cups_proxy/public/cpp/type_conversions.h" |
| #include "chrome/services/cups_proxy/socket_manager.h" |
| #include "chrome/services/cups_proxy/test/paths.h" |
| #include "net/base/io_buffer.h" |
| #include "net/socket/unix_domain_client_socket_posix.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace cups_proxy { |
| namespace { |
| |
| // Returns absl::nullopt on failure. |
| absl::optional<std::string> GetTestFile(std::string test_name) { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| |
| // Build file path. |
| base::FilePath path; |
| if (!base::PathService::Get(Paths::DIR_TEST_DATA, &path)) { |
| return absl::nullopt; |
| } |
| |
| path = path.Append(FILE_PATH_LITERAL(test_name)) |
| .AddExtension(FILE_PATH_LITERAL(".bin")); |
| |
| // Read in file contents. |
| std::string contents; |
| if (!base::ReadFileToString(path, &contents)) { |
| return absl::nullopt; |
| } |
| |
| return contents; |
| } |
| |
| } // namespace |
| |
| // Fake delegate granting handle to an IO-thread task runner. |
| class FakeServiceDelegate : public FakeCupsProxyServiceDelegate { |
| public: |
| FakeServiceDelegate() = default; |
| ~FakeServiceDelegate() override = default; |
| |
| // Note: Can't simulate actual IO thread in unit_tests, so we serve an |
| // arbitrary SingleThreadTaskRunner. |
| scoped_refptr<base::SingleThreadTaskRunner> GetIOTaskRunner() override { |
| return base::ThreadPool::CreateSingleThreadTaskRunner({}); |
| } |
| }; |
| |
| // Gives full control over the "CUPS daemon" in this test. |
| class FakeSocket : public net::UnixDomainClientSocket { |
| public: |
| FakeSocket() : UnixDomainClientSocket("", false) /* Dummy values */ {} |
| ~FakeSocket() override = default; |
| |
| // Saves expected request and corresponding response to send back. |
| void set_request(base::StringPiece request) { request_ = request; } |
| void set_response(base::StringPiece response) { response_ = response; } |
| |
| // Controls whether each method runs synchronously or asynchronously. |
| void set_connect_async() { connect_async = true; } |
| void set_read_async() { read_async = true; } |
| void set_write_async() { write_async = true; } |
| |
| // net::UnixDomainClientSocket overrides. |
| bool IsConnected() const override { return is_connected; } |
| |
| int Connect(net::CompletionOnceCallback callback) override { |
| if (is_connected) { |
| // Should've checked IsConnected first. |
| return net::ERR_FAILED; |
| } |
| |
| is_connected = true; |
| |
| // Sync |
| if (!connect_async) { |
| return net::OK; |
| } |
| |
| // Async |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&FakeSocket::OnAsyncCallback, base::Unretained(this), |
| std::move(callback), net::OK)); |
| return net::ERR_IO_PENDING; |
| } |
| |
| int Read(net::IOBuffer* buf, |
| int buf_len, |
| net::CompletionOnceCallback callback) override { |
| if (!is_connected) { |
| return net::ERR_FAILED; |
| } |
| |
| size_t num_to_read = |
| std::min(response_.size(), static_cast<size_t>(buf_len)); |
| std::copy(response_.begin(), response_.begin() + num_to_read, buf->data()); |
| response_.remove_prefix(num_to_read); |
| |
| // Sync |
| if (!read_async) { |
| return num_to_read; |
| } |
| |
| // Async |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&FakeSocket::OnAsyncCallback, base::Unretained(this), |
| std::move(callback), num_to_read)); |
| return net::ERR_IO_PENDING; |
| } |
| |
| int Write(net::IOBuffer* buf, |
| int buf_len, |
| net::CompletionOnceCallback callback, |
| const net::NetworkTrafficAnnotationTag& unused) override { |
| if (!is_connected) { |
| return net::ERR_FAILED; |
| } |
| |
| // Checks that |buf| holds (part of) the expected request. |
| if (!std::equal(buf->data(), buf->data() + buf_len, request_.begin())) { |
| return net::ERR_FAILED; |
| } |
| |
| // Arbitrary maximum write buffer size; just forcing partial writes. |
| const size_t kMaxWriteSize = 100; |
| size_t num_to_write = std::min(kMaxWriteSize, static_cast<size_t>(buf_len)); |
| request_.remove_prefix(num_to_write); |
| |
| // Sync |
| if (!write_async) { |
| return num_to_write; |
| } |
| |
| // Async |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&FakeSocket::OnAsyncCallback, base::Unretained(this), |
| std::move(callback), num_to_write)); |
| return net::ERR_IO_PENDING; |
| } |
| |
| // Generic callback used to force called methods to return asynchronously. |
| void OnAsyncCallback(net::CompletionOnceCallback callback, int net_code) { |
| std::move(callback).Run(net_code); |
| } |
| |
| private: |
| bool is_connected = false; |
| bool connect_async = false, read_async = false, write_async = false; |
| base::StringPiece request_, response_; |
| }; |
| |
| class SocketManagerTest : public testing::Test { |
| public: |
| SocketManagerTest() { |
| delegate_ = std::make_unique<FakeServiceDelegate>(); |
| |
| std::unique_ptr<FakeSocket> socket = std::make_unique<FakeSocket>(); |
| socket_ = socket.get(); |
| |
| manager_ = |
| SocketManager::CreateForTesting(std::move(socket), delegate_.get()); |
| } |
| |
| std::unique_ptr<std::vector<uint8_t>> ProxyToCups(std::string request) { |
| std::vector<uint8_t> request_as_bytes = |
| ipp_converter::ConvertToByteBuffer(request); |
| std::unique_ptr<std::vector<uint8_t>> response; |
| |
| base::RunLoop run_loop; |
| manager_->ProxyToCups(std::move(request_as_bytes), |
| base::BindOnce(&SocketManagerTest::OnProxyToCups, |
| weak_factory_.GetWeakPtr(), |
| run_loop.QuitClosure(), &response)); |
| run_loop.Run(); |
| return response; |
| } |
| |
| protected: |
| // Must be first member. |
| base::test::TaskEnvironment task_environment_; |
| |
| void OnProxyToCups(base::OnceClosure finish_cb, |
| std::unique_ptr<std::vector<uint8_t>>* ret, |
| std::unique_ptr<std::vector<uint8_t>> result) { |
| *ret = std::move(result); |
| std::move(finish_cb).Run(); |
| } |
| |
| // Fake injected service delegate. |
| std::unique_ptr<FakeServiceDelegate> delegate_; |
| |
| // Not owned. |
| FakeSocket* socket_; |
| |
| std::unique_ptr<SocketManager> manager_; |
| base::WeakPtrFactory<SocketManagerTest> weak_factory_{this}; |
| }; |
| |
| // "basic_handshake" test file contains a simple HTTP request sent by libCUPS, |
| // copied below for convenience: |
| // |
| // POST / HTTP/1.1 |
| // Content-Length: 72 |
| // Content-Type: application/ipp |
| // Date: Thu, 04 Oct 2018 20:25:59 GMT |
| // Host: localhost:0 |
| // User-Agent: CUPS/2.3b1 (Linux 4.4.159-15303-g65f4b5a7b3d3; i686) IPP/2.0 |
| // |
| // @Gattributes-charsetutf-8Hattributes-natural-languageen |
| |
| // All socket accesses are resolved synchronously. |
| TEST_F(SocketManagerTest, SyncEverything) { |
| // Read request & response |
| absl::optional<std::string> http_handshake = GetTestFile("basic_handshake"); |
| EXPECT_TRUE(http_handshake); |
| |
| // Pre-load |socket_| with request/response. |
| // TODO(crbug.com/495409): Test with actual http response. |
| socket_->set_request(*http_handshake); |
| socket_->set_response(*http_handshake); |
| |
| auto response = ProxyToCups(*http_handshake); |
| EXPECT_TRUE(response); |
| EXPECT_EQ(*response, ipp_converter::ConvertToByteBuffer(*http_handshake)); |
| } |
| |
| TEST_F(SocketManagerTest, AsyncEverything) { |
| auto http_handshake = GetTestFile("basic_handshake"); |
| EXPECT_TRUE(http_handshake); |
| |
| socket_->set_request(*http_handshake); |
| socket_->set_response(*http_handshake); |
| |
| // Set all |socket_| calls to run asynchronously. |
| socket_->set_connect_async(); |
| socket_->set_read_async(); |
| socket_->set_write_async(); |
| |
| auto response = ProxyToCups(*http_handshake); |
| EXPECT_TRUE(response); |
| EXPECT_EQ(*response, ipp_converter::ConvertToByteBuffer(*http_handshake)); |
| } |
| |
| } // namespace cups_proxy |