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); }