buffet: add unit tests for http_utils

Added more unit tests for utility functions in http_utils.h

BUG=chromium:367379
TEST=Old and new unit tests pass.

Change-Id: I04e28691bc4db3980783c0033f5691087a8c6ab3
Reviewed-on: https://chromium-review.googlesource.com/197312
Reviewed-by: Alex Vakulenko <avakulenko@chromium.org>
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/http_connection_fake.cc b/buffet/http_connection_fake.cc
index 6731aeb..0507c0b 100644
--- a/buffet/http_connection_fake.cc
+++ b/buffet/http_connection_fake.cc
@@ -67,7 +67,9 @@
 }
 
 uint64_t Connection::GetResponseDataSize() const {
-  return response_.GetData().size();
+  // HEAD requests must not return body.
+  return (request_.GetMethod() != request_type::kHead) ?
+      response_.GetData().size() : 0;
 }
 
 bool Connection::ReadResponseData(void* data, size_t buffer_size,
@@ -75,7 +77,8 @@
   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_to_read > 0)
+    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;
diff --git a/buffet/http_transport_fake.h b/buffet/http_transport_fake.h
index c526770..b95599f 100644
--- a/buffet/http_transport_fake.h
+++ b/buffet/http_transport_fake.h
@@ -81,6 +81,9 @@
   // Add/retrieve request/response HTTP headers.
   void AddHeaders(const HeaderList& headers);
   std::string GetHeader(const std::string& header_name) const;
+  const std::map<std::string, std::string>& GetHeaders() const {
+    return headers_;
+  }
 
  protected:
   // Data buffer.
diff --git a/buffet/http_utils_unittest.cc b/buffet/http_utils_unittest.cc
index c04f498..1453b86 100644
--- a/buffet/http_utils_unittest.cc
+++ b/buffet/http_utils_unittest.cc
@@ -2,18 +2,224 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <base/values.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/string_utils.h"
 #include "buffet/url_utils.h"
 
 using namespace chromeos;
 using namespace chromeos::http;
 
-static const char fake_url[] = "http://localhost";
+static const char kFakeUrl[] = "http://localhost";
+static const char kEchoUrl[] = "http://localhost/echo";
+static const char kMethodEchoUrl[] = "http://localhost/echo/method";
+
+///////////////////// Generic helper request handlers /////////////////////////
+// Returns the request data back with the same content type.
+static void EchoDataHandler(const fake::ServerRequest& request,
+                            fake::ServerResponse* response) {
+  response->Reply(status_code::Ok, request.GetData(),
+                  request.GetHeader(request_header::kContentType).c_str());
+};
+
+// Returns the request method as a plain text response.
+static void EchoMethodHandler(const fake::ServerRequest& request,
+                              fake::ServerResponse* response) {
+  response->ReplyText(status_code::Ok, request.GetMethod(), mime::text::kPlain);
+};
+
+///////////////////////////////////////////////////////////////////////////////
+TEST(HttpUtils, SendRequest_BinaryData) {
+  std::shared_ptr<fake::Transport> transport(new fake::Transport);
+  transport->AddHandler(kEchoUrl, request_type::kPost,
+                        base::Bind(EchoDataHandler));
+
+  // Test binary data round-tripping.
+  std::vector<unsigned char> custom_data{0xFF, 0x00, 0x80, 0x40, 0xC0, 0x7F};
+  auto response = http::SendRequest(request_type::kPost, kEchoUrl,
+                                    custom_data.data(), custom_data.size(),
+                                    mime::application::kOctet_stream,
+                                    HeaderList(), transport);
+  EXPECT_TRUE(response->IsSuccessful());
+  EXPECT_EQ(mime::application::kOctet_stream, response->GetContentType());
+  EXPECT_EQ(custom_data.size(), response->GetData().size());
+  EXPECT_EQ(custom_data, response->GetData());
+}
+
+TEST(HttpUtils, SendRequest_Post) {
+  std::shared_ptr<fake::Transport> transport(new fake::Transport);
+  transport->AddHandler(kMethodEchoUrl, "*", base::Bind(EchoMethodHandler));
+
+  // Test binary data round-tripping.
+  std::vector<unsigned char> custom_data{0xFF, 0x00, 0x80, 0x40, 0xC0, 0x7F};
+
+  // Check the correct HTTP method used.
+  auto response = http::SendRequest(request_type::kPost, kMethodEchoUrl,
+                                    custom_data.data(), custom_data.size(),
+                                    mime::application::kOctet_stream,
+                                    HeaderList(), transport);
+  EXPECT_TRUE(response->IsSuccessful());
+  EXPECT_EQ(mime::text::kPlain, response->GetContentType());
+  EXPECT_EQ(request_type::kPost, response->GetDataAsString());
+}
+
+TEST(HttpUtils, SendRequest_Get) {
+  std::shared_ptr<fake::Transport> transport(new fake::Transport);
+  transport->AddHandler(kMethodEchoUrl, "*", base::Bind(EchoMethodHandler));
+
+  auto response = http::SendRequest(request_type::kGet, kMethodEchoUrl,
+                                    nullptr, 0, nullptr,
+                                    HeaderList(), transport);
+  EXPECT_TRUE(response->IsSuccessful());
+  EXPECT_EQ(mime::text::kPlain, response->GetContentType());
+  EXPECT_EQ(request_type::kGet, response->GetDataAsString());
+}
+
+TEST(HttpUtils, SendRequest_Put) {
+  std::shared_ptr<fake::Transport> transport(new fake::Transport);
+  transport->AddHandler(kMethodEchoUrl, "*", base::Bind(EchoMethodHandler));
+
+  auto response = http::SendRequest(request_type::kPut, kMethodEchoUrl,
+                                    nullptr, 0, nullptr,
+                                    HeaderList(), transport);
+  EXPECT_TRUE(response->IsSuccessful());
+  EXPECT_EQ(mime::text::kPlain, response->GetContentType());
+  EXPECT_EQ(request_type::kPut, response->GetDataAsString());
+}
+
+TEST(HttpUtils, SendRequest_NotFound) {
+  std::shared_ptr<fake::Transport> transport(new fake::Transport);
+  // Test failed response (URL not found).
+  auto response = http::SendRequest(request_type::kGet, "http://blah.com",
+                                    nullptr, 0, nullptr,
+                                    HeaderList(), transport);
+  EXPECT_FALSE(response->IsSuccessful());
+  EXPECT_EQ(status_code::NotFound, response->GetStatusCode());
+}
+
+TEST(HttpUtils, SendRequest_Headers) {
+  std::shared_ptr<fake::Transport> transport(new fake::Transport);
+
+  static const char json_echo_url[] = "http://localhost/echo/json";
+  auto JsonEchoHandler = [](const fake::ServerRequest& request,
+                            fake::ServerResponse* response) {
+    base::DictionaryValue json;
+    json.SetString("method", request.GetMethod());
+    json.SetString("data", request.GetDataAsString());
+    for (auto&& pair : request.GetHeaders()) {
+      json.SetString("header." + pair.first, pair.second);
+    }
+    response->ReplyJson(status_code::Ok, &json);
+  };
+  transport->AddHandler(json_echo_url, "*",
+                        base::Bind(JsonEchoHandler));
+  auto response = http::SendRequest(
+      request_type::kPost, json_echo_url, "abcd", 4,
+      mime::application::kOctet_stream, {
+        {request_header::kCookie, "flavor=vanilla"},
+        {request_header::kIfMatch, "*"},
+      }, transport);
+  EXPECT_TRUE(response->IsSuccessful());
+  EXPECT_EQ(mime::application::kJson, response->GetContentType());
+  auto json = ParseJsonResponse(response.get(), nullptr, nullptr);
+  std::string value;
+  EXPECT_TRUE(json->GetString("method", &value));
+  EXPECT_EQ(request_type::kPost, value);
+  EXPECT_TRUE(json->GetString("data", &value));
+  EXPECT_EQ("abcd", value);
+  EXPECT_TRUE(json->GetString("header.Cookie", &value));
+  EXPECT_EQ("flavor=vanilla", value);
+  EXPECT_TRUE(json->GetString("header.Content-Type", &value));
+  EXPECT_EQ(mime::application::kOctet_stream, value);
+  EXPECT_TRUE(json->GetString("header.Content-Length", &value));
+  EXPECT_EQ("4", value);
+  EXPECT_TRUE(json->GetString("header.If-Match", &value));
+  EXPECT_EQ("*", value);
+}
+
+TEST(HttpUtils, Get) {
+  // Sends back the "?test=..." portion of URL.
+  // So if we do GET "http://localhost?test=blah", this handler responds
+  // with "blah" as text/plain.
+  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(kFakeUrl, request_type::kGet, base::Bind(GetHandler));
+  transport->AddHandler(kMethodEchoUrl, "*", base::Bind(EchoMethodHandler));
+
+  // Make sure Get/GetAsString actually do the GET request
+  auto response = http::Get(kMethodEchoUrl, transport);
+  EXPECT_TRUE(response->IsSuccessful());
+  EXPECT_EQ(mime::text::kPlain, response->GetContentType());
+  EXPECT_EQ(request_type::kGet, response->GetDataAsString());
+  EXPECT_EQ(request_type::kGet, http::GetAsString(kMethodEchoUrl, transport));
+
+  for (std::string data : {"blah", "some data", ""}) {
+    std::string url = url::AppendQueryParam(kFakeUrl, "test", data);
+    EXPECT_EQ(data, http::GetAsString(url, transport));
+  }
+}
+
+TEST(HttpUtils, Head) {
+  auto HeadHandler = [](const fake::ServerRequest& request,
+                        fake::ServerResponse* response) {
+    EXPECT_EQ(request_type::kHead, request.GetMethod());
+    EXPECT_EQ("0", request.GetHeader(request_header::kContentLength));
+    EXPECT_EQ("", request.GetHeader(request_header::kContentType));
+    response->ReplyText(status_code::Ok, "blah",
+                        mime::text::kPlain);
+  };
+
+  std::shared_ptr<fake::Transport> transport(new fake::Transport);
+  transport->AddHandler(kFakeUrl, request_type::kHead, base::Bind(HeadHandler));
+
+  auto response = http::Head(kFakeUrl, transport);
+  EXPECT_TRUE(response->IsSuccessful());
+  EXPECT_EQ(mime::text::kPlain, response->GetContentType());
+  EXPECT_EQ("", response->GetDataAsString()); // Must not have actual body.
+  EXPECT_EQ("4", response->GetHeader(request_header::kContentLength));
+}
+
+TEST(HttpUtils, PostBinary) {
+  auto Handler = [](const fake::ServerRequest& request,
+                    fake::ServerResponse* response) {
+    EXPECT_EQ(request_type::kPost, request.GetMethod());
+    EXPECT_EQ("256", request.GetHeader(request_header::kContentLength));
+    EXPECT_EQ(mime::application::kOctet_stream,
+              request.GetHeader(request_header::kContentType));
+    auto&& data = request.GetData();
+    EXPECT_EQ(256, data.size());
+
+    // Sum up all the bytes.
+    int sum = std::accumulate(data.begin(), data.end(), 0);
+    EXPECT_EQ(32640, sum); // sum(i, i => [0, 255]) = 32640.
+    response->ReplyText(status_code::Ok, "", mime::text::kPlain);
+  };
+
+  std::shared_ptr<fake::Transport> transport(new fake::Transport);
+  transport->AddHandler(kFakeUrl, request_type::kPost, base::Bind(Handler));
+
+  /// Fill the data buffer with bytes from 0x00 to 0xFF.
+  std::vector<unsigned char> data(256);
+  unsigned char counter = 0xFF;
+  std::generate(data.begin(), data.end(), [&counter]() { return ++counter; });
+
+  auto response = http::PostBinary(kFakeUrl, data.data(), data.size(),
+                                   transport);
+  EXPECT_TRUE(response->IsSuccessful());
+}
 
 TEST(HttpUtils, PostText) {
   std::string fake_data = "Some data";
@@ -21,37 +227,108 @@
                                  fake::ServerResponse* response) {
     EXPECT_EQ(request_type::kPost, request.GetMethod());
     EXPECT_EQ(fake_data.size(),
-              atoi(request.GetHeader(request_header::kContentLength).c_str()));
+              std::stoul(request.GetHeader(request_header::kContentLength)));
     EXPECT_EQ(mime::text::kPlain,
               request.GetHeader(request_header::kContentType));
-    response->Reply(status_code::Ok, request.GetData(), mime::text::kPlain);
+    response->ReplyText(status_code::Ok, request.GetDataAsString(),
+                       mime::text::kPlain);
   };
 
   std::shared_ptr<fake::Transport> transport(new fake::Transport);
-  transport->AddHandler(fake_url, request_type::kPost, base::Bind(PostHandler));
+  transport->AddHandler(kFakeUrl, request_type::kPost, base::Bind(PostHandler));
 
-  auto response = http::PostText(fake_url, fake_data.c_str(),
+  auto response = http::PostText(kFakeUrl, 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);
-  };
-
+TEST(HttpUtils, PostFormData) {
   std::shared_ptr<fake::Transport> transport(new fake::Transport);
-  transport->AddHandler(fake_url, request_type::kGet, base::Bind(GetHandler));
+  transport->AddHandler(kFakeUrl, request_type::kPost,
+                        base::Bind(EchoDataHandler));
 
-  for (std::string data : {"blah", "some data", ""}) {
-    std::string url = url::AppendQueryParam(fake_url, "test", data);
-    EXPECT_EQ(data, http::GetAsString(url, transport));
-  }
+  auto response = http::PostFormData(kFakeUrl, {
+                      {"key", "value"},
+                      {"field", "field value"},
+                  }, transport);
+  EXPECT_TRUE(response->IsSuccessful());
+  EXPECT_EQ(mime::application::kWwwFormUrlEncoded, response->GetContentType());
+  EXPECT_EQ("key=value&field=field+value", response->GetDataAsString());
 }
+
+TEST(HttpUtils, PostPatchJson) {
+  auto JsonHandler = [](const fake::ServerRequest& request,
+                        fake::ServerResponse* response) {
+    EXPECT_EQ(mime::application::kJson,
+              request.GetHeader(request_header::kContentType));
+    base::DictionaryValue json;
+    json.SetString("method", request.GetMethod());
+    json.SetString("data", request.GetDataAsString());
+    response->ReplyJson(status_code::Ok, &json);
+  };
+  std::shared_ptr<fake::Transport> transport(new fake::Transport);
+  transport->AddHandler(kFakeUrl, "*", base::Bind(JsonHandler));
+
+  base::DictionaryValue json;
+  json.SetString("key1", "val1");
+  json.SetString("key2", "val2");
+  std::string value;
+
+  // Test POST
+  auto response = http::PostJson(kFakeUrl, &json, transport);
+  auto resp_json = http::ParseJsonResponse(response.get(), nullptr, nullptr);
+  EXPECT_NE(nullptr, resp_json.get());
+  EXPECT_TRUE(resp_json->GetString("method", &value));
+  EXPECT_EQ(request_type::kPost, value);
+  EXPECT_TRUE(resp_json->GetString("data", &value));
+  EXPECT_EQ("{\"key1\":\"val1\",\"key2\":\"val2\"}", value);
+
+  // Test PATCH
+  response = http::PatchJson(kFakeUrl, &json, transport);
+  resp_json = http::ParseJsonResponse(response.get(), nullptr, nullptr);
+  EXPECT_NE(nullptr, resp_json.get());
+  EXPECT_TRUE(resp_json->GetString("method", &value));
+  EXPECT_EQ(request_type::kPatch, value);
+  EXPECT_TRUE(resp_json->GetString("data", &value));
+  EXPECT_EQ("{\"key1\":\"val1\",\"key2\":\"val2\"}", value);
+}
+
+TEST(HttpUtils, ParseJsonResponse) {
+  auto JsonHandler = [](const fake::ServerRequest& request,
+                        fake::ServerResponse* response) {
+    base::DictionaryValue json;
+    json.SetString("data", request.GetFormField("value"));
+    int status_code = std::stoi(request.GetFormField("code"));
+    response->ReplyJson(status_code, &json);
+  };
+  std::shared_ptr<fake::Transport> transport(new fake::Transport);
+  transport->AddHandler(kFakeUrl, request_type::kPost, base::Bind(JsonHandler));
+
+  // Test valid JSON responses (with success or error codes).
+  for (auto&& item : {"200;data", "400;wrong", "500;Internal Server error"}) {
+    auto pair = string_utils::SplitAtFirst(item, ';');
+    auto response = http::PostFormData(kFakeUrl, {
+                      {"code", pair.first},
+                      {"value", pair.second},
+                    }, transport);
+    int code = 0;
+    auto json = http::ParseJsonResponse(response.get(), &code, nullptr);
+    EXPECT_NE(nullptr, json.get());
+    std::string value;
+    EXPECT_TRUE(json->GetString("data", &value));
+    EXPECT_EQ(pair.first, std::to_string(code));
+    EXPECT_EQ(pair.second, value);
+  }
+
+  // Test invalid (non-JSON) reponse.
+  auto response = http::Get("http://bad.url", transport);
+  EXPECT_EQ(status_code::NotFound, response->GetStatusCode());
+  EXPECT_EQ(mime::text::kHtml, response->GetContentType());
+  int code = 0;
+  auto json = http::ParseJsonResponse(response.get(), &code, nullptr);
+  EXPECT_EQ(nullptr, json.get());
+  EXPECT_EQ(status_code::NotFound, code);
+}
+