Buffet: Implement fake HTTP transport to help write unit tests

Created fake Transport and Connection classes to help test
HTTP communications in Buffet. Now, when a fake transport
class is created a number of HTTP request handlers can be
registered, which will be called when a request to a web
server is made. These handlers can reply to the caller on
server's behalf and can provide response based on the
request data and parameters.

Removed 'static' from http::Request::range_value_omitted due
to a build break in debug (-O0) build. Static members should
be generally initialized in a .cc file, not header.

Fixed a bug in chromeos::url::GetQueryStringParameters() when
called on an empty string.

Finally, added 'bind_lamda.h' header file that adds the
ability to use lambdas in base::Bind() calls.

BUG=chromium:367377
TEST=Unit tests pass.

Change-Id: Ib4c070f676069f208b9df4da069ff3a29f8f656f
Reviewed-on: https://chromium-review.googlesource.com/197157
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/bind_lambda.h b/buffet/bind_lambda.h
new file mode 100644
index 0000000..69d948c
--- /dev/null
+++ b/buffet/bind_lambda.h
@@ -0,0 +1,65 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BUFFET_BIND_LAMBDA_H_
+#define BUFFET_BIND_LAMBDA_H_
+
+#include <base/bind.h>
+
+////////////////////////////////////////////////////////////////////////////////
+// This file is an extension to base/bind_internal.h and adds a RunnableAdapter
+// class specialization that wraps a functor (including lambda objects), so
+// they can be used in base::Callback/base::Bind constructs.
+// By including this file you will gain the ability to write expressions like:
+//    base::Callback<int(int)> callback = base::Bind([](int value) {
+//      return value * value;
+//    });
+////////////////////////////////////////////////////////////////////////////////
+namespace base {
+namespace internal {
+
+// LambdaAdapter is a helper class that specializes on different function call
+// signatures and provides the RunType and Run() method required by
+// RunnableAdapter<> class.
+template <typename Lambda, typename Sig>
+class LambdaAdapter;
+
+// R(...)
+template <typename Lambda, typename R, typename... Args>
+class LambdaAdapter<Lambda, R(Lambda::*)(Args... args)> {
+public:
+  typedef R(RunType)(Args...);
+  LambdaAdapter(Lambda lambda) : lambda_(lambda) {}
+  R Run(Args... args) { return lambda_(args...); }
+
+private:
+  Lambda lambda_;
+};
+
+// R(...) const
+template <typename Lambda, typename R, typename... Args>
+class LambdaAdapter<Lambda, R(Lambda::*)(Args... args) const> {
+public:
+  typedef R(RunType)(Args...);
+  LambdaAdapter(Lambda lambda) : lambda_(lambda) {}
+  R Run(Args... args) { return lambda_(args...); }
+
+private:
+  Lambda lambda_;
+};
+
+template <typename Lambda>
+class RunnableAdapter : public LambdaAdapter<Lambda,
+                                             decltype(&Lambda::operator())> {
+public:
+  explicit RunnableAdapter(Lambda lambda) :
+      LambdaAdapter<Lambda, decltype(&Lambda::operator())>(lambda) {
+  }
+};
+
+
+} // namespace internal
+} // namespace base
+
+#endif // BUFFET_BIND_LAMBDA_H_
diff --git a/buffet/buffet.gyp b/buffet/buffet.gyp
index 06e87f5..2288162 100644
--- a/buffet/buffet.gyp
+++ b/buffet/buffet.gyp
@@ -73,10 +73,12 @@
       ],
       'includes': ['../common-mk/common_test.gypi'],
       'sources': [
+        'async_event_sequencer_unittest.cc',
         'buffet_testrunner.cc',
         'data_encoding_unittest.cc',
         'exported_property_set_unittest.cc',
-        'async_event_sequencer_unittest.cc',
+        'http_connection_fake.cc',
+        'http_transport_fake.cc',
         'http_utils_unittest.cc',
         'mime_utils_unittest.cc',
         'string_utils_unittest.cc',
diff --git a/buffet/http_connection_fake.cc b/buffet/http_connection_fake.cc
new file mode 100644
index 0000000..6731aeb
--- /dev/null
+++ b/buffet/http_connection_fake.cc
@@ -0,0 +1,87 @@
+// Copyright 2014 The Chromium OS 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 "buffet/http_connection_fake.h"
+
+#include <base/logging.h>
+
+#include "buffet/http_request.h"
+#include "buffet/mime_utils.h"
+#include "buffet/string_utils.h"
+
+using namespace chromeos;
+using namespace chromeos::http::fake;
+
+Connection::Connection(const std::string& url, const std::string& method,
+                       std::shared_ptr<http::Transport> transport) :
+    http::Connection(transport), request_(url, method) {
+  VLOG(1) << "fake::Connection created: " << method;
+}
+
+Connection::~Connection() {
+  VLOG(1) << "fake::Connection destroyed";
+}
+
+bool Connection::SendHeaders(const HeaderList& headers) {
+  request_.AddHeaders(headers);
+  return true;
+}
+
+bool Connection::WriteRequestData(const void* data, size_t size) {
+  request_.AddData(data, size);
+  return true;
+}
+
+bool Connection::FinishRequest() {
+  request_.AddHeaders({{request_header::kContentLength,
+                      std::to_string(request_.GetData().size())}});
+  fake::Transport* transport = dynamic_cast<fake::Transport*>(transport_.get());
+  CHECK(transport) << "Expecting a fake transport";
+  auto handler = transport->GetHandler(request_.GetURL(), request_.GetMethod());
+  if (handler.is_null()) {
+    response_.ReplyText(status_code::NotFound,
+                        "<html><body>Not found</body></html>",
+                        mime::text::kHtml);
+  } else {
+    handler.Run(request_, &response_);
+  }
+  return true;
+}
+
+int Connection::GetResponseStatusCode() const {
+  return response_.GetStatusCode();
+}
+
+std::string Connection::GetResponseStatusText() const {
+  return response_.GetStatusText();
+}
+
+std::string Connection::GetProtocolVersion() const {
+  return response_.GetProtocolVersion();
+}
+
+std::string Connection::GetResponseHeader(
+    const std::string& header_name) const {
+  return response_.GetHeader(header_name);
+}
+
+uint64_t Connection::GetResponseDataSize() const {
+  return response_.GetData().size();
+}
+
+bool Connection::ReadResponseData(void* data, size_t buffer_size,
+                                  size_t* size_read) {
+  size_t size_to_read = GetResponseDataSize() - response_data_ptr_;
+  if (size_to_read > buffer_size)
+    size_to_read = buffer_size;
+  memcpy(data, response_.GetData().data() + response_data_ptr_, size_to_read);
+  if (size_read)
+    *size_read = size_to_read;
+  response_data_ptr_ += size_to_read;
+  return true;
+}
+
+std::string Connection::GetErrorMessage() const {
+  return std::string();
+}
diff --git a/buffet/http_connection_fake.h b/buffet/http_connection_fake.h
new file mode 100644
index 0000000..26ca307
--- /dev/null
+++ b/buffet/http_connection_fake.h
@@ -0,0 +1,62 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BUFFET_HTTP_CONNECTION_FAKE_H_
+#define BUFFET_HTTP_CONNECTION_FAKE_H_
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include <base/basictypes.h>
+
+#include "buffet/http_connection.h"
+#include "buffet/http_transport_fake.h"
+
+namespace chromeos {
+namespace http {
+namespace fake {
+
+// This is a fake implementation of http::Connection for unit testing.
+class Connection : public chromeos::http::Connection {
+ public:
+  Connection(const std::string& url, const std::string& method,
+             std::shared_ptr<http::Transport> transport);
+  virtual ~Connection();
+
+  // Overrides from http::Connection.
+  // See http_connection.h for description of these methods.
+  virtual bool SendHeaders(const HeaderList& headers) override;
+  virtual bool WriteRequestData(const void* data, size_t size) override;
+  virtual bool FinishRequest() override;
+
+  virtual int GetResponseStatusCode() const override;
+  virtual std::string GetResponseStatusText() const override;
+  virtual std::string GetProtocolVersion() const override;
+  virtual std::string GetResponseHeader(
+     const std::string& header_name) const override;
+  virtual uint64_t GetResponseDataSize() const override;
+  virtual bool ReadResponseData(void* data, size_t buffer_size,
+                                size_t* size_read) override;
+  virtual std::string GetErrorMessage() const override;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(Connection);
+
+  // Request and response objects passed to the user-provided request handler
+  // callback. The request object contains all the request information.
+  // The response object is the server response that is created by
+  // the handler in response to the request.
+  ServerRequest request_;
+  ServerResponse response_;
+
+  // Internal read data pointer needed for ReadResponseData() implementation.
+  size_t response_data_ptr_ = 0;
+};
+
+}  // namespace fake
+}  // namespace http
+}  // namespace chromeos
+
+#endif // BUFFET_HTTP_CONNECTION_FAKE_H_
diff --git a/buffet/http_request.h b/buffet/http_request.h
index a93af94..62f4a01 100644
--- a/buffet/http_request.h
+++ b/buffet/http_request.h
@@ -305,7 +305,7 @@
   // range_value_omitted is used in |ranges_| list to indicate omitted value.
   // E.g. range (10,range_value_omitted) represents bytes from 10 to the end
   // of the data stream.
-  static const uint64_t range_value_omitted = (uint64_t)-1;
+  const uint64_t range_value_omitted = (uint64_t)-1;
 
   // Error message in case request fails completely.
   std::string error_;
diff --git a/buffet/http_transport_fake.cc b/buffet/http_transport_fake.cc
new file mode 100644
index 0000000..57278b7
--- /dev/null
+++ b/buffet/http_transport_fake.cc
@@ -0,0 +1,210 @@
+// Copyright 2014 The Chromium OS 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 "buffet/http_transport_fake.h"
+
+#include <base/json/json_writer.h>
+
+#include "buffet/http_connection_fake.h"
+#include "buffet/http_request.h"
+#include "buffet/mime_utils.h"
+#include "buffet/url_utils.h"
+
+using namespace chromeos;
+using namespace chromeos::http::fake;
+
+Transport::Transport() {
+  VLOG(1) << "fake::Transport created";
+}
+
+Transport::~Transport() {
+  VLOG(1) << "fake::Transport destroyed";
+}
+
+std::unique_ptr<http::Connection> Transport::CreateConnection(
+    std::shared_ptr<http::Transport> transport,
+    const std::string& url,
+    const std::string& method,
+    const HeaderList& headers,
+    const std::string& user_agent,
+    const std::string& referer,
+    std::string* error_msg) {
+  HeaderList headers_copy = headers;
+  if (!user_agent.empty()) {
+    headers_copy.push_back(std::make_pair(http::request_header::kUserAgent,
+                                          user_agent));
+  }
+  if (!referer.empty()) {
+    headers_copy.push_back(std::make_pair(http::request_header::kReferer,
+                                          referer));
+  }
+  std::unique_ptr<http::Connection> connection(
+      new http::fake::Connection(url, method, transport));
+  CHECK(connection) << "Unable to create Connection object";
+  if (!connection->SendHeaders(headers_copy)) {
+    connection.reset();
+    if (error_msg)
+      *error_msg = "Failed to send request headers";
+  }
+  return connection;
+}
+
+static inline std::string GetHandlerMapKey(const std::string& url,
+                                           const std::string& method) {
+  return method + ":" + url;
+}
+
+void Transport::AddHandler(const std::string& url, const std::string& method,
+                           const HandlerCallback& handler) {
+  handlers_.insert(std::make_pair(GetHandlerMapKey(url, method), handler));
+}
+
+Transport::HandlerCallback Transport::GetHandler(
+    const std::string& url, const std::string& method) const {
+  // First try the exact combination of URL/Method
+  auto p = handlers_.find(GetHandlerMapKey(url, method));
+  if (p != handlers_.end())
+    return p->second;
+  // If not found, try URL/*
+  p = handlers_.find(GetHandlerMapKey(url, "*"));
+  if (p != handlers_.end())
+    return p->second;
+  // If still not found, try */method
+  p = handlers_.find(GetHandlerMapKey("*", method));
+  if (p != handlers_.end())
+    return p->second;
+  // Finally, try */*
+  p = handlers_.find(GetHandlerMapKey("*", "*"));
+  return (p != handlers_.end()) ? p->second : HandlerCallback();
+}
+
+void ServerRequestResponseBase::AddData(const void* data, size_t data_size) {
+  auto bytes = reinterpret_cast<const unsigned char*>(data);
+  data_.insert(data_.end(), bytes, bytes + data_size);
+}
+
+std::string ServerRequestResponseBase::GetDataAsString() const {
+  if (data_.empty())
+    return std::string();
+  auto chars = reinterpret_cast<const char*>(data_.data());
+  return std::string(chars, data_.size());
+}
+
+void ServerRequestResponseBase::AddHeaders(const HeaderList& headers) {
+  for (auto&& pair : headers) {
+    if (pair.second.empty())
+      headers_.erase(pair.first);
+    else
+      headers_.insert(pair);
+  }
+}
+
+std::string ServerRequestResponseBase::GetHeader(
+    const std::string& header_name) const {
+  auto p = headers_.find(header_name);
+  return p != headers_.end() ? p->second : std::string();
+}
+
+ServerRequest::ServerRequest(const std::string& url,
+                             const std::string& method) : method_(method) {
+  auto params = url::GetQueryStringParameters(url);
+  url_ = url::RemoveQueryString(url, true);
+  form_fields_.insert(params.begin(), params.end());
+}
+
+std::string ServerRequest::GetFormField(const std::string& field_name) const {
+  if (!form_fields_parsed_) {
+    std::string mime_type = mime::RemoveParameters(
+        GetHeader(request_header::kContentType));
+    if (mime_type == mime::application::kWwwFormUrlEncoded &&
+        !GetData().empty()) {
+      auto fields = data_encoding::WebParamsDecode(GetDataAsString());
+      form_fields_.insert(fields.begin(), fields.end());
+    }
+    form_fields_parsed_ = true;
+  }
+  auto p = form_fields_.find(field_name);
+  return p != form_fields_.end() ? p->second : std::string();
+}
+
+void ServerResponse::Reply(int status_code, const void* data, size_t data_size,
+                           const char* mime_type) {
+  data_.clear();
+  status_code_ = status_code;
+  AddData(data, data_size);
+  AddHeaders({
+    {response_header::kContentLength, std::to_string(data_size)},
+    {response_header::kContentType, mime_type}
+  });
+}
+
+void ServerResponse::ReplyText(int status_code, const std::string& text,
+                               const char* mime_type) {
+  Reply(status_code, text.data(), text.size(), mime_type);
+}
+
+void ServerResponse::ReplyJson(int status_code, const base::Value* json) {
+  std::string text;
+  base::JSONWriter::WriteWithOptions(json,
+                                     base::JSONWriter::OPTIONS_PRETTY_PRINT,
+                                     &text);
+  ReplyText(status_code, text, mime::application::kJson);
+}
+
+std::string ServerResponse::GetStatusText() const {
+  static std::vector<std::pair<int, const char*>> status_text_map = {
+    {100, "Continue"},
+    {101, "Switching Protocols"},
+    {102, "Processing"},
+    {200, "OK"},
+    {201, "Created"},
+    {202, "Accepted"},
+    {203, "Non-Authoritative Information"},
+    {204, "No Content"},
+    {205, "Reset Content"},
+    {206, "Partial Content"},
+    {207, "Multi-Status"},
+    {208, "Already Reported"},
+    {226, "IM Used"},
+    {300, "Multiple Choices"},
+    {301, "Moved Permanently"},
+    {302, "Found"},
+    {303, "See Other"},
+    {304, "Not Modified"},
+    {305, "Use Proxy"},
+    {306, "Switch Proxy"},
+    {307, "Temporary Redirect"},
+    {308, "Permanent Redirect"},
+    {400, "Bad Request"},
+    {401, "Unauthorized"},
+    {402, "Payment Required"},
+    {403, "Forbidden"},
+    {404, "Not Found"},
+    {405, "Method Not Allowed"},
+    {406, "Not Acceptable"},
+    {407, "Proxy Authentication Required"},
+    {408, "Request Timeout"},
+    {409, "Conflict"},
+    {410, "Gone"},
+    {411, "Length Required"},
+    {412, "Precondition Failed"},
+    {413, "Request Entity Too Large"},
+    {414, "Request - URI Too Long"},
+    {415, "Unsupported Media Type"},
+    {429, "Too Many Requests"},
+    {431, "Request Header Fields Too Large"},
+    {500, "Internal Server Error"},
+    {501, "Not Implemented"},
+    {502, "Bad Gateway"},
+    {503, "Service Unavailable"},
+    {504, "Gateway Timeout"},
+    {505, "HTTP Version Not Supported"},
+  };
+
+  for (auto&& pair : status_text_map) {
+    if (pair.first == status_code_)
+      return pair.second;
+  }
+  return std::string();
+}
diff --git a/buffet/http_transport_fake.h b/buffet/http_transport_fake.h
new file mode 100644
index 0000000..c526770
--- /dev/null
+++ b/buffet/http_transport_fake.h
@@ -0,0 +1,198 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BUFFET_HTTP_TRANSPORT_FAKE_H_
+#define BUFFET_HTTP_TRANSPORT_FAKE_H_
+
+#include <type_traits>
+
+#include <base/callback.h>
+#include <base/values.h>
+
+#include "buffet/http_transport.h"
+
+namespace chromeos {
+namespace http {
+namespace fake {
+
+class ServerRequest;
+class ServerResponse;
+class Connection;
+
+///////////////////////////////////////////////////////////////////////////////
+// A fake implementation of http::Transport that simulates HTTP communication
+// with a server.
+///////////////////////////////////////////////////////////////////////////////
+class Transport : public http::Transport {
+ public:
+  Transport();
+  virtual ~Transport();
+
+  // Server handler callback signature.
+  typedef base::Callback<void(const ServerRequest&, ServerResponse*)>
+      HandlerCallback;
+
+  // This method allows the test code to provide a callback to handle requests
+  // for specific URL/HTTP-verb combination. When a specific |method| request
+  // is made on the given |url|, the |handler| will be invoked and all the
+  // request data will be filled in the |ServerRequest| parameter. Any server
+  // response should be returned through the |ServerResponse| parameter.
+  // Either |method| or |url| (or both) can be specified as "*" to handle
+  // any requests. So, ("http://localhost","*") will handle any request type
+  // on that URL and ("*","GET") will handle any GET requests.
+  // The lookup starts with the most specific data pair to the catch-all (*,*).
+  void AddHandler(const std::string& url, const std::string& method,
+                  const HandlerCallback& handler);
+  // Retrieve a handler for specific |url| and request |method|.
+  HandlerCallback GetHandler(const std::string& url,
+                             const std::string& method) const;
+
+  // Overload from http::Transport
+  virtual std::unique_ptr<http::Connection> CreateConnection(
+      std::shared_ptr<http::Transport> transport,
+      const std::string& url,
+      const std::string& method,
+      const HeaderList& headers,
+      const std::string& user_agent,
+      const std::string& referer,
+      std::string* error_msg) override;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(Transport);
+
+  // A list of user-supplied request handlers.
+  std::map<std::string, HandlerCallback> handlers_;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// A base class for ServerRequest and ServerResponse. It provides common
+// functionality to work with request/response HTTP headers and data.
+///////////////////////////////////////////////////////////////////////////////
+class ServerRequestResponseBase {
+ public:
+  ServerRequestResponseBase() = default;
+
+  // Add/retrieve request/response body data.
+  void AddData(const void* data, size_t data_size);
+  const std::vector<unsigned char>& GetData() const { return data_; }
+  std::string GetDataAsString() const;
+
+  // Add/retrieve request/response HTTP headers.
+  void AddHeaders(const HeaderList& headers);
+  std::string GetHeader(const std::string& header_name) const;
+
+ protected:
+  // Data buffer.
+  std::vector<unsigned char> data_;
+  // Header map.
+  std::map<std::string, std::string> headers_;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(ServerRequestResponseBase);
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// A container class that encapsulates all the HTTP server request information.
+///////////////////////////////////////////////////////////////////////////////
+class ServerRequest : public ServerRequestResponseBase {
+ public:
+  ServerRequest(const std::string& url, const std::string& method);
+
+  // Get the actual request URL. Does not include the query string or fragment.
+  const std::string& GetURL() const { return url_; }
+  // Get the request method.
+  const std::string& GetMethod() const { return method_; }
+  // Get the POST/GET request parameters. These are parsed query string
+  // parameters from the URL. In addition, for POST requests with
+  // application/x-www-form-urlencoded content type, the request body is also
+  // parsed and individual fields can be accessed through this method.
+  std::string GetFormField(const std::string& field_name) const;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(ServerRequest);
+
+  // Request URL (without query string or URL fragment).
+  std::string url_;
+  // Request method
+  std::string method_;
+  // List of available request data form fields.
+  mutable std::map<std::string, std::string> form_fields_;
+  // Flag used on first request to GetFormField to parse the body of HTTP POST
+  // request with application/x-www-form-urlencoded content.
+  mutable bool form_fields_parsed_ = false;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// A container class that encapsulates all the HTTP server response information.
+// The request handler will use this class to provide a response to the caller.
+// Call the Reply() or the approriate ReplyNNN() specialization to provide
+// the response data. Additional calls to AddHeaders() can be made to provide
+// custom response headers. The Reply-methods will already provide the
+// followig response headers:
+//    Content-Length
+//    Content-Type
+///////////////////////////////////////////////////////////////////////////////
+class ServerResponse : public ServerRequestResponseBase {
+public:
+  ServerResponse() = default;
+
+  // Generic reply method.
+  void Reply(int status_code, const void* data, size_t data_size,
+             const char* mime_type);
+  // Reply with text body.
+  void ReplyText(int status_code, const std::string& text,
+                 const char* mime_type);
+  // Reply with JSON object. The content type will be "application/json".
+  void ReplyJson(int status_code, const base::Value* json);
+
+  // Specialized overload to send the binary data as an array of simple
+  // data elements. Only trivial data types (scalars, POD structures, etc)
+  // can be used.
+  template<typename T>
+  void Reply(int status_code, const std::vector<T>& data,
+             const char* mime_type) {
+    // Make sure T doesn't have virtual functions, custom constructors, etc.
+    static_assert(std::is_trivial<T>::value, "Only simple data is supported");
+    Reply(status_code, data.data(), data.size() * sizeof(T), mime_type);
+  }
+
+  // Specialized overload to send the binary data.
+  // Only trivial data types (scalars, POD structures, etc) can be used.
+  template<typename T>
+  void Reply(int status_code, const T& data, const char* mime_type) {
+    // Make sure T doesn't have virtual functions, custom constructors, etc.
+    static_assert(std::is_trivial<T>::value, "Only simple data is supported");
+    Reply(status_code, &data, sizeof(T), mime_type);
+  }
+
+  // For handlers that want to simulate versions of HTTP protocol other
+  // than HTTP/1.1, call this method with the custom version string,
+  // for example "HTTP/1.0".
+  void SetProtocolVersion(const std::string& protocol_version) {
+    protocol_version_ = protocol_version;
+  }
+
+ protected:
+  // These methods are helpers to implement corresponding functionality
+  // of fake::Connection.
+  friend class Connection;
+  // Helper for fake::Connection::GetResponseStatusCode().
+  int GetStatusCode() const { return status_code_; }
+  // Helper for fake::Connection::GetResponseStatusText().
+  std::string GetStatusText() const;
+  // Helper for fake::Connection::GetProtocolVersion().
+  std::string GetProtocolVersion() const { return protocol_version_; }
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(ServerResponse);
+
+  int status_code_ = 0;
+  std::string protocol_version_ = "HTTP/1.1";
+};
+
+} // namespace fake
+} // namespace http
+} // namespace chromeos
+
+#endif // BUFFET_HTTP_TRANSPORT_FAKE_H_
diff --git a/buffet/http_utils_unittest.cc b/buffet/http_utils_unittest.cc
index ab137a7..c04f498 100644
--- a/buffet/http_utils_unittest.cc
+++ b/buffet/http_utils_unittest.cc
@@ -2,12 +2,56 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "buffet/http_utils.h"
-
 #include <gtest/gtest.h>
 
+#include "buffet/bind_lambda.h"
+#include "buffet/http_utils.h"
+#include "buffet/http_transport_fake.h"
+#include "buffet/mime_utils.h"
+#include "buffet/url_utils.h"
+
+using namespace chromeos;
 using namespace chromeos::http;
 
-TEST(HttpUtils, SendRequest) {
-  // TODO(avakulenko)
+static const char fake_url[] = "http://localhost";
+
+TEST(HttpUtils, PostText) {
+  std::string fake_data = "Some data";
+  auto PostHandler = [fake_data](const fake::ServerRequest& request,
+                                 fake::ServerResponse* response) {
+    EXPECT_EQ(request_type::kPost, request.GetMethod());
+    EXPECT_EQ(fake_data.size(),
+              atoi(request.GetHeader(request_header::kContentLength).c_str()));
+    EXPECT_EQ(mime::text::kPlain,
+              request.GetHeader(request_header::kContentType));
+    response->Reply(status_code::Ok, request.GetData(), mime::text::kPlain);
+  };
+
+  std::shared_ptr<fake::Transport> transport(new fake::Transport);
+  transport->AddHandler(fake_url, request_type::kPost, base::Bind(PostHandler));
+
+  auto response = http::PostText(fake_url, fake_data.c_str(),
+                                 mime::text::kPlain, transport);
+  EXPECT_TRUE(response->IsSuccessful());
+  EXPECT_EQ(mime::text::kPlain, response->GetContentType());
+  EXPECT_EQ(fake_data, response->GetDataAsString());
+}
+
+TEST(HttpUtils, Get) {
+  auto GetHandler = [](const fake::ServerRequest& request,
+                       fake::ServerResponse* response) {
+    EXPECT_EQ(request_type::kGet, request.GetMethod());
+    EXPECT_EQ("0", request.GetHeader(request_header::kContentLength));
+    EXPECT_EQ("", request.GetHeader(request_header::kContentType));
+    response->ReplyText(status_code::Ok, request.GetFormField("test"),
+                       mime::text::kPlain);
+  };
+
+  std::shared_ptr<fake::Transport> transport(new fake::Transport);
+  transport->AddHandler(fake_url, request_type::kGet, base::Bind(GetHandler));
+
+  for (std::string data : {"blah", "some data", ""}) {
+    std::string url = url::AppendQueryParam(fake_url, "test", data);
+    EXPECT_EQ(data, http::GetAsString(url, transport));
+  }
 }
diff --git a/buffet/url_utils.cc b/buffet/url_utils.cc
index 08d78f7..aea0d9d 100644
--- a/buffet/url_utils.cc
+++ b/buffet/url_utils.cc
@@ -96,7 +96,9 @@
 chromeos::data_encoding::WebParamList chromeos::url::GetQueryStringParameters(
     const std::string& url) {
   // Extract the query string and remove the leading '?'.
-  std::string query_string = GetQueryString(url, true).substr(1);
+  std::string query_string = GetQueryString(url, true);
+  if (!query_string.empty() && query_string.front() == '?')
+    query_string.erase(query_string.begin());
   return chromeos::data_encoding::WebParamsDecode(query_string);
 }