chromium/chrome/services/cups_proxy/socket_manager_unittest.cc

// Copyright 2019 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/services/cups_proxy/socket_manager.h"

#include <algorithm>
#include <memory>
#include <string>
#include <string_view>
#include <utility>

#include "base/files/file_util.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/test/task_environment.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/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 std::nullopt on failure.
std::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 std::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 std::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(std::string_view request) { request_ = request; }
  void set_response(std::string_view 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::SequencedTaskRunner::GetCurrentDefault()->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::SequencedTaskRunner::GetCurrentDefault()->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::SequencedTaskRunner::GetCurrentDefault()->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;
  std::string_view 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.
  raw_ptr<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
  std::optional<std::string> http_handshake = GetTestFile("basic_handshake");
  EXPECT_TRUE(http_handshake);

  // Pre-load |socket_| with request/response.
  // TODO(crbug.com/41179657): 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