summaryrefslogtreecommitdiffstats
path: root/net
diff options
context:
space:
mode:
authorphajdan.jr@chromium.org <phajdan.jr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-05-07 17:50:26 +0000
committerphajdan.jr@chromium.org <phajdan.jr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-05-07 17:50:26 +0000
commitb6e5b6c6feec3301c6e74ae76e1df1762b150c3a (patch)
tree2c69567a4b2ca6b86d4e563e05dd3cbfcee4160a /net
parent04e51aa91468b3ad25e36ab7ac712be285495104 (diff)
downloadchromium_src-b6e5b6c6feec3301c6e74ae76e1df1762b150c3a.zip
chromium_src-b6e5b6c6feec3301c6e74ae76e1df1762b150c3a.tar.gz
chromium_src-b6e5b6c6feec3301c6e74ae76e1df1762b150c3a.tar.bz2
GTTF: move chrome/browser/google_apis/test_server to net/test
This is a part of replacing most usages of the Python test server with an in-process C++ test server that should be easier to debug. BUG=96594 R=rch@chromium.org, satorux@chromium.org Review URL: https://codereview.chromium.org/14971002 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@198762 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'net')
-rw-r--r--net/net.gyp11
-rw-r--r--net/test/embedded_test_server/OWNERS2
-rw-r--r--net/test/embedded_test_server/http_connection.cc35
-rw-r--r--net/test/embedded_test_server/http_connection.h59
-rw-r--r--net/test/embedded_test_server/http_request.cc202
-rw-r--r--net/test/embedded_test_server/http_request.h115
-rw-r--r--net/test/embedded_test_server/http_request_unittest.cc82
-rw-r--r--net/test/embedded_test_server/http_response.cc54
-rw-r--r--net/test/embedded_test_server/http_response.h71
-rw-r--r--net/test/embedded_test_server/http_response_unittest.cc31
-rw-r--r--net/test/embedded_test_server/http_server.cc213
-rw-r--r--net/test/embedded_test_server/http_server.h161
-rw-r--r--net/test/embedded_test_server/http_server_unittest.cc223
13 files changed, 1259 insertions, 0 deletions
diff --git a/net/net.gyp b/net/net.gyp
index d13d4e8..75179aa 100644
--- a/net/net.gyp
+++ b/net/net.gyp
@@ -1771,6 +1771,9 @@
'ssl/ssl_cipher_suite_names_unittest.cc',
'ssl/ssl_client_auth_cache_unittest.cc',
'ssl/ssl_config_service_unittest.cc',
+ 'test/embedded_test_server/http_request_unittest.cc',
+ 'test/embedded_test_server/http_response_unittest.cc',
+ 'test/embedded_test_server/http_server_unittest.cc',
'test/python_utils_unittest.cc',
'test/run_all_unittests.cc',
'test/test_certificate_data.h',
@@ -2137,6 +2140,14 @@
'test/base_test_server.h',
'test/cert_test_util.cc',
'test/cert_test_util.h',
+ 'test/embedded_test_server/http_connection.cc',
+ 'test/embedded_test_server/http_connection.h',
+ 'test/embedded_test_server/http_request.cc',
+ 'test/embedded_test_server/http_request.h',
+ 'test/embedded_test_server/http_response.cc',
+ 'test/embedded_test_server/http_response.h',
+ 'test/embedded_test_server/http_server.cc',
+ 'test/embedded_test_server/http_server.h',
'test/local_test_server_posix.cc',
'test/local_test_server_win.cc',
'test/local_test_server.cc',
diff --git a/net/test/embedded_test_server/OWNERS b/net/test/embedded_test_server/OWNERS
new file mode 100644
index 0000000..e0abbce
--- /dev/null
+++ b/net/test/embedded_test_server/OWNERS
@@ -0,0 +1,2 @@
+mtomasz@chromium.org
+satorux@chromium.org
diff --git a/net/test/embedded_test_server/http_connection.cc b/net/test/embedded_test_server/http_connection.cc
new file mode 100644
index 0000000..3207aac
--- /dev/null
+++ b/net/test/embedded_test_server/http_connection.cc
@@ -0,0 +1,35 @@
+// Copyright (c) 2012 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 "net/test/embedded_test_server/http_connection.h"
+
+#include "net/socket/stream_listen_socket.h"
+#include "net/test/embedded_test_server/http_response.h"
+
+namespace google_apis {
+namespace test_server {
+
+HttpConnection::HttpConnection(net::StreamListenSocket* socket,
+ const HandleRequestCallback& callback)
+ : socket_(socket),
+ callback_(callback) {
+}
+
+HttpConnection::~HttpConnection() {
+}
+
+void HttpConnection::SendResponse(scoped_ptr<HttpResponse> response) const {
+ const std::string response_string = response->ToResponseString();
+ socket_->Send(response_string.c_str(), response_string.length());
+}
+
+void HttpConnection::ReceiveData(const base::StringPiece& data) {
+ request_parser_.ProcessChunk(data);
+ if (request_parser_.ParseRequest() == HttpRequestParser::ACCEPTED) {
+ callback_.Run(this, request_parser_.GetRequest());
+ }
+}
+
+} // namespace test_server
+} // namespace google_apis
diff --git a/net/test/embedded_test_server/http_connection.h b/net/test/embedded_test_server/http_connection.h
new file mode 100644
index 0000000..2561a3f
--- /dev/null
+++ b/net/test/embedded_test_server/http_connection.h
@@ -0,0 +1,59 @@
+// Copyright (c) 2012 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.
+
+#ifndef NET_TEST_EMBEDDED_TEST_SERVER_HTTP_CONNECTION_H_
+#define NET_TEST_EMBEDDED_TEST_SERVER_HTTP_CONNECTION_H_
+
+#include "base/basictypes.h"
+#include "base/callback.h"
+#include "base/memory/ref_counted.h"
+#include "base/strings/string_piece.h"
+#include "net/test/embedded_test_server/http_request.h"
+
+namespace net {
+class StreamListenSocket;
+}
+
+namespace google_apis {
+namespace test_server {
+
+class HttpConnection;
+class HttpResponse;
+
+// Calblack called when a request is parsed. Response should be sent
+// using HttpConnection::SendResponse() on the |connection| argument.
+typedef base::Callback<void(HttpConnection* connection,
+ scoped_ptr<HttpRequest> request)>
+ HandleRequestCallback;
+
+// Wraps the connection socket. Accepts incoming data and sends responses.
+// If a valid request is parsed, then |callback_| is invoked.
+class HttpConnection {
+ public:
+ HttpConnection(net::StreamListenSocket* socket,
+ const HandleRequestCallback& callback);
+ ~HttpConnection();
+
+ // Sends the HTTP response to the client.
+ void SendResponse(scoped_ptr<HttpResponse> response) const;
+
+ private:
+ friend class HttpServer;
+
+ // Accepts raw chunk of data from the client. Internally, passes it to the
+ // HttpRequestParser class. If a request is parsed, then |callback_| is
+ // called.
+ void ReceiveData(const base::StringPiece& data);
+
+ scoped_refptr<net::StreamListenSocket> socket_;
+ const HandleRequestCallback callback_;
+ HttpRequestParser request_parser_;
+
+ DISALLOW_COPY_AND_ASSIGN(HttpConnection);
+};
+
+} // namespace test_server
+} // namespace google_apis
+
+#endif // NET_TEST_EMBEDDED_TEST_SERVER_HTTP_CONNECTION_H_
diff --git a/net/test/embedded_test_server/http_request.cc b/net/test/embedded_test_server/http_request.cc
new file mode 100644
index 0000000..39c8512
--- /dev/null
+++ b/net/test/embedded_test_server/http_request.cc
@@ -0,0 +1,202 @@
+// Copyright (c) 2012 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 "net/test/embedded_test_server/http_request.h"
+
+#include <algorithm>
+
+#include "base/logging.h"
+#include "base/string_util.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
+
+namespace google_apis {
+namespace test_server {
+
+namespace {
+
+size_t kRequestSizeLimit = 64 * 1024 * 1024; // 64 mb.
+
+// Helper function used to trim tokens in http request headers.
+std::string Trim(const std::string& value) {
+ std::string result;
+ TrimString(value, " \t", &result);
+ return result;
+}
+
+} // namespace
+
+HttpRequest::HttpRequest() : method(METHOD_UNKNOWN),
+ has_content(false) {
+}
+
+HttpRequest::~HttpRequest() {
+}
+
+HttpRequestParser::HttpRequestParser()
+ : http_request_(new HttpRequest()),
+ buffer_position_(0),
+ state_(STATE_HEADERS),
+ declared_content_length_(0) {
+}
+
+HttpRequestParser::~HttpRequestParser() {
+}
+
+void HttpRequestParser::ProcessChunk(const base::StringPiece& data) {
+ data.AppendToString(&buffer_);
+ DCHECK_LE(buffer_.size() + data.size(), kRequestSizeLimit) <<
+ "The HTTP request is too large.";
+}
+
+std::string HttpRequestParser::ShiftLine() {
+ size_t eoln_position = buffer_.find("\r\n", buffer_position_);
+ DCHECK_NE(std::string::npos, eoln_position);
+ const int line_length = eoln_position - buffer_position_;
+ std::string result = buffer_.substr(buffer_position_, line_length);
+ buffer_position_ += line_length + 2;
+ return result;
+}
+
+HttpRequestParser::ParseResult HttpRequestParser::ParseRequest() {
+ DCHECK_NE(STATE_ACCEPTED, state_);
+ // Parse the request from beginning. However, entire request may not be
+ // available in the buffer.
+ if (state_ == STATE_HEADERS) {
+ if (ParseHeaders() == ACCEPTED)
+ return ACCEPTED;
+ }
+ // This should not be 'else if' of the previous block, as |state_| can be
+ // changed in ParseHeaders().
+ if (state_ == STATE_CONTENT) {
+ if (ParseContent() == ACCEPTED)
+ return ACCEPTED;
+ }
+ return WAITING;
+}
+
+HttpRequestParser::ParseResult HttpRequestParser::ParseHeaders() {
+ // Check if the all request headers are available.
+ if (buffer_.find("\r\n\r\n", buffer_position_) == std::string::npos)
+ return WAITING;
+
+ // Parse request's the first header line.
+ // Request main main header, eg. GET /foobar.html HTTP/1.1
+ {
+ const std::string header_line = ShiftLine();
+ std::vector<std::string> header_line_tokens;
+ base::SplitString(header_line, ' ', &header_line_tokens);
+ DCHECK_EQ(3u, header_line_tokens.size());
+ // Method.
+ http_request_->method = GetMethodType(StringToLowerASCII(
+ header_line_tokens[0]));
+ // Address.
+ // Don't build an absolute URL as the parser does not know (should not
+ // know) anything about the server address.
+ http_request_->relative_url = header_line_tokens[1];
+ // Protocol.
+ const std::string protocol = StringToLowerASCII(header_line_tokens[2]);
+ CHECK(protocol == "http/1.0" || protocol == "http/1.1") <<
+ "Protocol not supported: " << protocol;
+ }
+
+ // Parse further headers.
+ {
+ std::string header_name;
+ while (true) {
+ std::string header_line = ShiftLine();
+ if (header_line.empty())
+ break;
+
+ if (header_line[0] == ' ' || header_line[0] == '\t') {
+ // Continuation of the previous multi-line header.
+ std::string header_value =
+ Trim(header_line.substr(1, header_line.size() - 1));
+ http_request_->headers[header_name] += " " + header_value;
+ } else {
+ // New header.
+ size_t delimiter_pos = header_line.find(":");
+ DCHECK_NE(std::string::npos, delimiter_pos) << "Syntax error.";
+ header_name = Trim(header_line.substr(0, delimiter_pos));
+ std::string header_value = Trim(header_line.substr(
+ delimiter_pos + 1,
+ header_line.size() - delimiter_pos - 1));
+ http_request_->headers[header_name] = header_value;
+ }
+ }
+ }
+
+ // Headers done. Is any content data attached to the request?
+ declared_content_length_ = 0;
+ if (http_request_->headers.count("Content-Length") > 0) {
+ http_request_->has_content = true;
+ const bool success = base::StringToSizeT(
+ http_request_->headers["Content-Length"],
+ &declared_content_length_);
+ DCHECK(success) << "Malformed Content-Length header's value.";
+ }
+ if (declared_content_length_ == 0) {
+ // No content data, so parsing is finished.
+ state_ = STATE_ACCEPTED;
+ return ACCEPTED;
+ }
+
+ // The request has not yet been parsed yet, content data is still to be
+ // processed.
+ state_ = STATE_CONTENT;
+ return WAITING;
+}
+
+HttpRequestParser::ParseResult HttpRequestParser::ParseContent() {
+ const size_t available_bytes = buffer_.size() - buffer_position_;
+ const size_t fetch_bytes = std::min(
+ available_bytes,
+ declared_content_length_ - http_request_->content.size());
+ http_request_->content.append(buffer_.data() + buffer_position_,
+ fetch_bytes);
+ buffer_position_ += fetch_bytes;
+
+ if (declared_content_length_ == http_request_->content.size()) {
+ state_ = STATE_ACCEPTED;
+ return ACCEPTED;
+ }
+
+ state_ = STATE_CONTENT;
+ return WAITING;
+}
+
+scoped_ptr<HttpRequest> HttpRequestParser::GetRequest() {
+ DCHECK_EQ(STATE_ACCEPTED, state_);
+ scoped_ptr<HttpRequest> result = http_request_.Pass();
+
+ // Prepare for parsing a new request.
+ state_ = STATE_HEADERS;
+ http_request_.reset(new HttpRequest());
+ buffer_.clear();
+ buffer_position_ = 0;
+ declared_content_length_ = 0;
+
+ return result.Pass();
+}
+
+HttpMethod HttpRequestParser::GetMethodType(const std::string& token) const {
+ if (token == "get") {
+ return METHOD_GET;
+ } else if (token == "head") {
+ return METHOD_HEAD;
+ } else if (token == "post") {
+ return METHOD_POST;
+ } else if (token == "put") {
+ return METHOD_PUT;
+ } else if (token == "delete") {
+ return METHOD_DELETE;
+ } else if (token == "patch") {
+ return METHOD_PATCH;
+ }
+ NOTREACHED() << "Method not implemented: " << token;
+ return METHOD_UNKNOWN;
+}
+
+} // namespace test_server
+} // namespace google_apis
diff --git a/net/test/embedded_test_server/http_request.h b/net/test/embedded_test_server/http_request.h
new file mode 100644
index 0000000..796d41a
--- /dev/null
+++ b/net/test/embedded_test_server/http_request.h
@@ -0,0 +1,115 @@
+// Copyright (c) 2012 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.
+
+#ifndef NET_TEST_EMBEDDED_TEST_SERVER_HTTP_REQUEST_H_
+#define NET_TEST_EMBEDDED_TEST_SERVER_HTTP_REQUEST_H_
+
+#include <map>
+#include <string>
+
+#include "base/basictypes.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/strings/string_piece.h"
+
+namespace google_apis {
+namespace test_server {
+
+// Methods of HTTP requests supported by the test HTTP server.
+enum HttpMethod {
+ METHOD_UNKNOWN,
+ METHOD_GET,
+ METHOD_HEAD,
+ METHOD_POST,
+ METHOD_PUT,
+ METHOD_DELETE,
+ METHOD_PATCH,
+};
+
+// Represents a HTTP request. Since it can be big, use scoped_ptr to pass it
+// instead of copying. However, the struct is copyable so tests can save and
+// examine a HTTP request.
+struct HttpRequest {
+ HttpRequest();
+ ~HttpRequest();
+
+ std::string relative_url; // Starts with '/'. Example: "/test?query=foo"
+ HttpMethod method;
+ std::map<std::string, std::string> headers;
+ std::string content;
+ bool has_content;
+};
+
+// Parses the input data and produces a valid HttpRequest object. If there is
+// more than one request in one chunk, then only the first one will be parsed.
+// The common use is as below:
+// HttpRequestParser parser;
+// (...)
+// void OnDataChunkReceived(Socket* socket, const char* data, int size) {
+// parser.ProcessChunk(std::string(data, size));
+// if (parser.ParseRequest() == HttpRequestParser::ACCEPTED) {
+// scoped_ptr<HttpRequest> request = parser.GetRequest();
+// (... process the request ...)
+// }
+class HttpRequestParser {
+ public:
+ // Parsing result.
+ enum ParseResult {
+ WAITING, // A request is not completed yet, waiting for more data.
+ ACCEPTED, // A request has been parsed and it is ready to be processed.
+ };
+
+ // Parser state.
+ enum State {
+ STATE_HEADERS, // Waiting for a request headers.
+ STATE_CONTENT, // Waiting for content data.
+ STATE_ACCEPTED, // Request has been parsed.
+ };
+
+ HttpRequestParser();
+ ~HttpRequestParser();
+
+ // Adds chunk of data into the internal buffer.
+ void ProcessChunk(const base::StringPiece& data);
+
+ // Parses the http request (including data - if provided).
+ // If returns ACCEPTED, then it means that the whole request has been found
+ // in the internal buffer (and parsed). After calling GetRequest(), it will be
+ // ready to parse another request.
+ ParseResult ParseRequest();
+
+ // Retrieves parsed request. Can be only called, when the parser is in
+ // STATE_ACCEPTED state. After calling it, the parser is ready to parse
+ // another request.
+ scoped_ptr<HttpRequest> GetRequest();
+
+ private:
+ HttpMethod GetMethodType(const std::string& token) const;
+
+ // Parses headers and returns ACCEPTED if whole request was parsed. Otherwise
+ // returns WAITING.
+ ParseResult ParseHeaders();
+
+ // Parses request's content data and returns ACCEPTED if all of it have been
+ // processed. Chunked Transfer Encoding *is not* supported.
+ ParseResult ParseContent();
+
+ // Fetches the next line from the buffer. Result does not contain \r\n.
+ // Returns an empty string for an empty line. It will assert if there is
+ // no line available.
+ std::string ShiftLine();
+
+ scoped_ptr<HttpRequest> http_request_;
+ std::string buffer_;
+ size_t buffer_position_; // Current position in the internal buffer.
+ State state_;
+ // Content length of the request currently being parsed.
+ size_t declared_content_length_;
+
+ DISALLOW_COPY_AND_ASSIGN(HttpRequestParser);
+};
+
+} // namespace test_server
+} // namespace google_apis
+
+#endif // NET_TEST_EMBEDDED_TEST_SERVER_HTTP_REQUEST_H_
diff --git a/net/test/embedded_test_server/http_request_unittest.cc b/net/test/embedded_test_server/http_request_unittest.cc
new file mode 100644
index 0000000..7229bf4
--- /dev/null
+++ b/net/test/embedded_test_server/http_request_unittest.cc
@@ -0,0 +1,82 @@
+// Copyright (c) 2012 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 "net/test/embedded_test_server/http_request.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace google_apis {
+namespace test_server {
+
+TEST(HttpRequestTest, ParseRequest) {
+ HttpRequestParser parser;
+
+ // Process request in chunks to check if the parser deals with border cases.
+ // Also, check multi-line headers as well as multiple requests in the same
+ // chunk. This basically should cover all the simplest border cases.
+ parser.ProcessChunk("POST /foobar.html HTTP/1.1\r\n");
+ EXPECT_EQ(HttpRequestParser::WAITING, parser.ParseRequest());
+ parser.ProcessChunk("Host: localhost:1234\r\n");
+ EXPECT_EQ(HttpRequestParser::WAITING, parser.ParseRequest());
+ parser.ProcessChunk("Multi-line-header: abcd\r\n");
+ EXPECT_EQ(HttpRequestParser::WAITING, parser.ParseRequest());
+ parser.ProcessChunk(" efgh\r\n");
+ EXPECT_EQ(HttpRequestParser::WAITING, parser.ParseRequest());
+ parser.ProcessChunk(" ijkl\r\n");
+ EXPECT_EQ(HttpRequestParser::WAITING, parser.ParseRequest());
+ parser.ProcessChunk("Content-Length: 10\r\n\r\n");
+ EXPECT_EQ(HttpRequestParser::WAITING, parser.ParseRequest());
+ // Content data and another request in the same chunk (possible in http/1.1).
+ parser.ProcessChunk("1234567890GET /another.html HTTP/1.1\r\n\r\n");
+ ASSERT_EQ(HttpRequestParser::ACCEPTED, parser.ParseRequest());
+
+ // Fetch the first request and validate it.
+ {
+ scoped_ptr<HttpRequest> request = parser.GetRequest();
+ EXPECT_EQ("/foobar.html", request->relative_url);
+ EXPECT_EQ(METHOD_POST, request->method);
+ EXPECT_EQ("1234567890", request->content);
+ ASSERT_EQ(3u, request->headers.size());
+
+ EXPECT_EQ(1u, request->headers.count("Host"));
+ EXPECT_EQ(1u, request->headers.count("Multi-line-header"));
+ EXPECT_EQ(1u, request->headers.count("Content-Length"));
+
+ EXPECT_EQ("localhost:1234", request->headers["Host"]);
+ EXPECT_EQ("abcd efgh ijkl", request->headers["Multi-line-header"]);
+ EXPECT_EQ("10", request->headers["Content-Length"]);
+ }
+
+ // No other request available yet since we do not support multiple requests
+ // per connection.
+ EXPECT_EQ(HttpRequestParser::WAITING, parser.ParseRequest());
+}
+
+TEST(HttpRequestTest, ParseRequestWithEmptyBody) {
+ HttpRequestParser parser;
+
+ parser.ProcessChunk("POST /foobar.html HTTP/1.1\r\n");
+ parser.ProcessChunk("Content-Length: 0\r\n\r\n");
+ ASSERT_EQ(HttpRequestParser::ACCEPTED, parser.ParseRequest());
+
+ scoped_ptr<HttpRequest> request = parser.GetRequest();
+ EXPECT_EQ("", request->content);
+ EXPECT_TRUE(request->has_content);
+ EXPECT_EQ(1u, request->headers.count("Content-Length"));
+ EXPECT_EQ("0", request->headers["Content-Length"]);
+}
+
+TEST(HttpRequestTest, ParseRequestWithoutBody) {
+ HttpRequestParser parser;
+
+ parser.ProcessChunk("POST /foobar.html HTTP/1.1\r\n\r\n");
+ ASSERT_EQ(HttpRequestParser::ACCEPTED, parser.ParseRequest());
+
+ scoped_ptr<HttpRequest> request = parser.GetRequest();
+ EXPECT_EQ("", request->content);
+ EXPECT_FALSE(request->has_content);
+}
+
+} // namespace test_server
+} // namespace google_apis
diff --git a/net/test/embedded_test_server/http_response.cc b/net/test/embedded_test_server/http_response.cc
new file mode 100644
index 0000000..72f465d
--- /dev/null
+++ b/net/test/embedded_test_server/http_response.cc
@@ -0,0 +1,54 @@
+// Copyright (c) 2012 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 "net/test/embedded_test_server/http_response.h"
+
+#include "base/format_macros.h"
+#include "base/logging.h"
+#include "base/stringprintf.h"
+
+namespace google_apis {
+namespace test_server {
+
+HttpResponse::HttpResponse() : code_(SUCCESS) {
+}
+
+HttpResponse::~HttpResponse() {
+}
+
+std::string HttpResponse::ToResponseString() const {
+ // Response line with headers.
+ std::string response_builder;
+
+ // TODO(mtomasz): For http/1.0 requests, send http/1.0.
+ // TODO(mtomasz): For different codes, send a corrent string instead of OK.
+ base::StringAppendF(&response_builder, "HTTP/1.1 %d OK\r\n", code_);
+ base::StringAppendF(&response_builder, "Connection: closed\r\n");
+ base::StringAppendF(&response_builder,
+ "Content-Length: %"PRIuS"\r\n",
+ content_.size());
+ base::StringAppendF(&response_builder,
+ "Content-Type: %s\r\n",
+ content_type_.c_str());
+ for (std::map<std::string, std::string>::const_iterator it =
+ custom_headers_.begin();
+ it != custom_headers_.end();
+ ++it) {
+ // Multi-line header value support.
+ const std::string& header_name = it->first;
+ const std::string& header_value = it->second;
+ DCHECK(header_value.find_first_of("\n\r") == std::string::npos) <<
+ "Malformed header value.";
+ base::StringAppendF(&response_builder,
+ "%s: %s\r\n",
+ header_name.c_str(),
+ header_value.c_str());
+ }
+ base::StringAppendF(&response_builder, "\r\n");
+
+ return response_builder + content_;
+}
+
+} // namespace test_server
+} // namespace google_apis
diff --git a/net/test/embedded_test_server/http_response.h b/net/test/embedded_test_server/http_response.h
new file mode 100644
index 0000000..4598587
--- /dev/null
+++ b/net/test/embedded_test_server/http_response.h
@@ -0,0 +1,71 @@
+// Copyright (c) 2012 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.
+
+#ifndef NET_TEST_EMBEDDED_TEST_SERVER_HTTP_RESPONSE_H_
+#define NET_TEST_EMBEDDED_TEST_SERVER_HTTP_RESPONSE_H_
+
+#include <map>
+#include <string>
+
+#include "base/basictypes.h"
+
+namespace google_apis {
+namespace test_server {
+
+enum ResponseCode {
+ SUCCESS = 200,
+ CREATED = 201,
+ NO_CONTENT = 204,
+ MOVED = 301,
+ RESUME_INCOMPLETE = 308,
+ NOT_FOUND = 404,
+ PRECONDITION = 412,
+ ACCESS_DENIED = 500,
+};
+
+// Respresents a HTTP response. Since it can be big, it may be better to use
+// scoped_ptr to pass it instead of copying.
+class HttpResponse {
+ public:
+ HttpResponse();
+ ~HttpResponse();
+
+ // The response code.
+ ResponseCode code() const { return code_; }
+ void set_code(ResponseCode code) { code_ = code; }
+
+ // The content of the response.
+ const std::string& content() const { return content_; }
+ void set_content(const std::string& content) { content_ = content; }
+
+ // The content type.
+ const std::string& content_type() const { return content_type_; }
+ void set_content_type(const std::string& content_type) {
+ content_type_ = content_type;
+ }
+
+ // The custom headers to be sent to the client.
+ const std::map<std::string, std::string>& custom_headers() const {
+ return custom_headers_;
+ }
+
+ // Adds a custom header.
+ void AddCustomHeader(const std::string& key, const std::string& value) {
+ custom_headers_[key] = value;
+ }
+
+ // Generates and returns a http response string.
+ std::string ToResponseString() const;
+
+ private:
+ ResponseCode code_;
+ std::string content_;
+ std::string content_type_;
+ std::map<std::string, std::string> custom_headers_;
+};
+
+} // namespace test_servers
+} // namespace google_apis
+
+#endif // NET_TEST_EMBEDDED_TEST_SERVER_HTTP_RESPONSE_H_
diff --git a/net/test/embedded_test_server/http_response_unittest.cc b/net/test/embedded_test_server/http_response_unittest.cc
new file mode 100644
index 0000000..b8f1a4b
--- /dev/null
+++ b/net/test/embedded_test_server/http_response_unittest.cc
@@ -0,0 +1,31 @@
+// Copyright (c) 2012 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 "net/test/embedded_test_server/http_response.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace google_apis {
+namespace test_server {
+
+TEST(HttpResponseTest, GenerateResponse) {
+ HttpResponse response;
+ response.set_code(SUCCESS);
+ response.set_content("Sample content - Hello world!");
+ response.set_content_type("text/plain");
+ response.AddCustomHeader("Simple-Header", "Simple value.");
+
+ std::string kExpectedResponseString =
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: closed\r\n"
+ "Content-Length: 29\r\n"
+ "Content-Type: text/plain\r\n"
+ "Simple-Header: Simple value.\r\n\r\n"
+ "Sample content - Hello world!";
+
+ EXPECT_EQ(kExpectedResponseString, response.ToResponseString());
+}
+
+} // namespace test_server
+} // namespace google_apis
diff --git a/net/test/embedded_test_server/http_server.cc b/net/test/embedded_test_server/http_server.cc
new file mode 100644
index 0000000..165ac1b
--- /dev/null
+++ b/net/test/embedded_test_server/http_server.cc
@@ -0,0 +1,213 @@
+// Copyright (c) 2012 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 "net/test/embedded_test_server/http_server.h"
+
+#include "base/bind.h"
+#include "base/run_loop.h"
+#include "base/stl_util.h"
+#include "base/string_util.h"
+#include "base/stringprintf.h"
+#include "net/test/embedded_test_server/http_connection.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
+#include "net/tools/fetch/http_listen_socket.h"
+
+namespace google_apis {
+namespace test_server {
+
+namespace {
+
+const int kPort = 8040;
+const char kIp[] = "127.0.0.1";
+const int kRetries = 10;
+
+// Callback to handle requests with default predefined response for requests
+// matching the address |url|.
+scoped_ptr<HttpResponse> HandleDefaultRequest(const GURL& url,
+ const HttpResponse& response,
+ const HttpRequest& request) {
+ const GURL request_url = url.Resolve(request.relative_url);
+ if (url.path() != request_url.path())
+ return scoped_ptr<HttpResponse>(NULL);
+ return scoped_ptr<HttpResponse>(new HttpResponse(response));
+}
+
+} // namespace
+
+HttpListenSocket::HttpListenSocket(const SocketDescriptor socket_descriptor,
+ net::StreamListenSocket::Delegate* delegate)
+ : net::TCPListenSocket(socket_descriptor, delegate) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+}
+
+void HttpListenSocket::Listen() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ net::TCPListenSocket::Listen();
+}
+
+HttpListenSocket::~HttpListenSocket() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+}
+
+HttpServer::HttpServer(
+ const scoped_refptr<base::SingleThreadTaskRunner>& io_thread)
+ : io_thread_(io_thread),
+ port_(-1),
+ weak_factory_(this) {
+ DCHECK(io_thread_);
+ DCHECK(thread_checker_.CalledOnValidThread());
+}
+
+HttpServer::~HttpServer() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+}
+
+bool HttpServer::InitializeAndWaitUntilReady() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ base::RunLoop run_loop;
+ if (!io_thread_->PostTaskAndReply(
+ FROM_HERE,
+ base::Bind(&HttpServer::InitializeOnIOThread, base::Unretained(this)),
+ run_loop.QuitClosure())) {
+ return false;
+ }
+ run_loop.Run();
+
+ return Started();
+}
+
+bool HttpServer::ShutdownAndWaitUntilComplete() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+
+ base::RunLoop run_loop;
+ if (!io_thread_->PostTaskAndReply(
+ FROM_HERE,
+ base::Bind(&HttpServer::ShutdownOnIOThread, base::Unretained(this)),
+ run_loop.QuitClosure())) {
+ return false;
+ }
+ run_loop.Run();
+
+ return true;
+}
+
+void HttpServer::InitializeOnIOThread() {
+ DCHECK(io_thread_->BelongsToCurrentThread());
+ DCHECK(!Started());
+
+ int retries_left = kRetries + 1;
+ int try_port = kPort;
+
+ while (retries_left > 0) {
+ SocketDescriptor socket_descriptor = net::TCPListenSocket::CreateAndBind(
+ kIp,
+ try_port);
+ if (socket_descriptor != net::TCPListenSocket::kInvalidSocket) {
+ listen_socket_ = new HttpListenSocket(socket_descriptor, this);
+ listen_socket_->Listen();
+ base_url_ = GURL(base::StringPrintf("http://%s:%d", kIp, try_port));
+ port_ = try_port;
+ break;
+ }
+ retries_left--;
+ try_port++;
+ }
+}
+
+void HttpServer::ShutdownOnIOThread() {
+ DCHECK(io_thread_->BelongsToCurrentThread());
+
+ listen_socket_ = NULL; // Release the listen socket.
+ STLDeleteContainerPairSecondPointers(connections_.begin(),
+ connections_.end());
+ connections_.clear();
+}
+
+void HttpServer::HandleRequest(HttpConnection* connection,
+ scoped_ptr<HttpRequest> request) {
+ DCHECK(io_thread_->BelongsToCurrentThread());
+
+ for (size_t i = 0; i < request_handlers_.size(); ++i) {
+ scoped_ptr<HttpResponse> response =
+ request_handlers_[i].Run(*request.get());
+ if (response.get()) {
+ connection->SendResponse(response.Pass());
+ return;
+ }
+ }
+
+ LOG(WARNING) << "Request not handled. Returning 404: "
+ << request->relative_url;
+ scoped_ptr<HttpResponse> not_found_response(new HttpResponse());
+ not_found_response->set_code(NOT_FOUND);
+ connection->SendResponse(not_found_response.Pass());
+
+ // Drop the connection, since we do not support multiple requests per
+ // connection.
+ connections_.erase(connection->socket_.get());
+ delete connection;
+}
+
+GURL HttpServer::GetURL(const std::string& relative_url) const {
+ DCHECK(StartsWithASCII(relative_url, "/", true /* case_sensitive */))
+ << relative_url;
+ return base_url_.Resolve(relative_url);
+}
+
+void HttpServer::RegisterRequestHandler(
+ const HandleRequestCallback& callback) {
+ request_handlers_.push_back(callback);
+}
+
+void HttpServer::DidAccept(net::StreamListenSocket* server,
+ net::StreamListenSocket* connection) {
+ DCHECK(io_thread_->BelongsToCurrentThread());
+
+ HttpConnection* http_connection = new HttpConnection(
+ connection,
+ base::Bind(&HttpServer::HandleRequest, weak_factory_.GetWeakPtr()));
+ connections_[connection] = http_connection;
+}
+
+void HttpServer::DidRead(net::StreamListenSocket* connection,
+ const char* data,
+ int length) {
+ DCHECK(io_thread_->BelongsToCurrentThread());
+
+ HttpConnection* http_connection = FindConnection(connection);
+ if (http_connection == NULL) {
+ LOG(WARNING) << "Unknown connection.";
+ return;
+ }
+ http_connection->ReceiveData(std::string(data, length));
+}
+
+void HttpServer::DidClose(net::StreamListenSocket* connection) {
+ DCHECK(io_thread_->BelongsToCurrentThread());
+
+ HttpConnection* http_connection = FindConnection(connection);
+ if (http_connection == NULL) {
+ LOG(WARNING) << "Unknown connection.";
+ return;
+ }
+ delete http_connection;
+ connections_.erase(connection);
+}
+
+HttpConnection* HttpServer::FindConnection(
+ net::StreamListenSocket* socket) {
+ DCHECK(io_thread_->BelongsToCurrentThread());
+
+ std::map<net::StreamListenSocket*, HttpConnection*>::iterator it =
+ connections_.find(socket);
+ if (it == connections_.end()) {
+ return NULL;
+ }
+ return it->second;
+}
+
+} // namespace test_server
+} // namespace google_apis
diff --git a/net/test/embedded_test_server/http_server.h b/net/test/embedded_test_server/http_server.h
new file mode 100644
index 0000000..d0e262e
--- /dev/null
+++ b/net/test/embedded_test_server/http_server.h
@@ -0,0 +1,161 @@
+// Copyright (c) 2012 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.
+
+#ifndef NET_TEST_EMBEDDED_TEST_SERVER_HTTP_SERVER_H_
+#define NET_TEST_EMBEDDED_TEST_SERVER_HTTP_SERVER_H_
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/compiler_specific.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "base/threading/thread_checker.h"
+#include "googleurl/src/gurl.h"
+#include "net/socket/tcp_listen_socket.h"
+
+namespace google_apis {
+namespace test_server {
+
+class HttpConnection;
+class HttpResponse;
+struct HttpRequest;
+
+// This class is required to be able to have composition instead of inheritance,
+class HttpListenSocket : public net::TCPListenSocket {
+ public:
+ HttpListenSocket(const SocketDescriptor socket_descriptor,
+ net::StreamListenSocket::Delegate* delegate);
+ virtual void Listen();
+
+ private:
+ virtual ~HttpListenSocket();
+
+ base::ThreadChecker thread_checker_;
+};
+
+// Class providing an HTTP server for testing purpose. This is a basic server
+// providing only an essential subset of HTTP/1.1 protocol. Especially,
+// it assumes that the request syntax is correct. It *does not* support
+// a Chunked Transfer Encoding.
+//
+// The common use case is below:
+//
+// base::Thread io_thread_;
+// scoped_ptr<HttpServer> test_server_;
+//
+// void SetUp() {
+// base::Thread::Options thread_options;
+// thread_options.message_loop_type = MessageLoop::TYPE_IO;
+// ASSERT_TRUE(io_thread_.StartWithOptions(thread_options));
+//
+// test_server_.reset(new HttpServer(io_thread_.message_loop_proxy()));
+// ASSERT_TRUE(test_server_.InitializeAndWaitUntilReady());
+// test_server_->RegisterRequestHandler(
+// base::Bind(&FooTest::HandleRequest, base::Unretained(this)));
+// }
+//
+// scoped_ptr<HttpResponse> HandleRequest(const HttpRequest& request) {
+// GURL absolute_url = test_server_->GetURL(request.relative_url);
+// if (absolute_url.path() != "/test")
+// return scoped_ptr<HttpResponse>();
+//
+// scoped_ptr<HttpResponse> http_response(new HttpResponse());
+// http_response->set_code(test_server::SUCCESS);
+// http_response->set_content("hello");
+// http_response->set_content_type("text/plain");
+// return http_response.Pass();
+// }
+//
+class HttpServer : public net::StreamListenSocket::Delegate {
+ public:
+ typedef base::Callback<scoped_ptr<HttpResponse>(const HttpRequest& request)>
+ HandleRequestCallback;
+
+ // Creates a http test server. |io_thread| is a task runner
+ // with IO message loop, used as a backend thread.
+ // InitializeAndWaitUntilReady() must be called to start the server.
+ explicit HttpServer(
+ const scoped_refptr<base::SingleThreadTaskRunner>& io_thread);
+ virtual ~HttpServer();
+
+ // Initializes and waits until the server is ready to accept requests.
+ bool InitializeAndWaitUntilReady() WARN_UNUSED_RESULT;
+
+ // Shuts down the http server and waits until the shutdown is complete.
+ bool ShutdownAndWaitUntilComplete() WARN_UNUSED_RESULT;
+
+ // Checks if the server is started.
+ bool Started() const {
+ return listen_socket_.get() != NULL;
+ }
+
+ // Returns the base URL to the server, which looks like
+ // http://127.0.0.1:<port>/, where <port> is the actual port number used by
+ // the server.
+ const GURL& base_url() const { return base_url_; }
+
+ // Returns a URL to the server based on the given relative URL, which
+ // should start with '/'. For example: GetURL("/path?query=foo") =>
+ // http://127.0.0.1:<port>/path?query=foo.
+ GURL GetURL(const std::string& relative_url) const;
+
+ // Returns the port number used by the server.
+ int port() const { return port_; }
+
+ // The most general purpose method. Any request processing can be added using
+ // this method. Takes ownership of the object. The |callback| is called
+ // on UI thread.
+ void RegisterRequestHandler(const HandleRequestCallback& callback);
+
+ private:
+ // Initializes and starts the server. If initialization succeeds, Starts()
+ // will return true.
+ void InitializeOnIOThread();
+
+ // Shuts down the server.
+ void ShutdownOnIOThread();
+
+ // Handles a request when it is parsed. It passes the request to registed
+ // request handlers and sends a http response.
+ void HandleRequest(HttpConnection* connection,
+ scoped_ptr<HttpRequest> request);
+
+ // net::StreamListenSocket::Delegate overrides:
+ virtual void DidAccept(net::StreamListenSocket* server,
+ net::StreamListenSocket* connection) OVERRIDE;
+ virtual void DidRead(net::StreamListenSocket* connection,
+ const char* data,
+ int length) OVERRIDE;
+ virtual void DidClose(net::StreamListenSocket* connection) OVERRIDE;
+
+ HttpConnection* FindConnection(net::StreamListenSocket* socket);
+
+ scoped_refptr<base::SingleThreadTaskRunner> io_thread_;
+
+ scoped_refptr<HttpListenSocket> listen_socket_;
+ int port_;
+ GURL base_url_;
+
+ // Owns the HttpConnection objects.
+ std::map<net::StreamListenSocket*, HttpConnection*> connections_;
+
+ // Vector of registered request handlers.
+ std::vector<HandleRequestCallback> request_handlers_;
+
+ // Note: This should remain the last member so it'll be destroyed and
+ // invalidate its weak pointers before any other members are destroyed.
+ base::WeakPtrFactory<HttpServer> weak_factory_;
+
+ base::ThreadChecker thread_checker_;
+
+ DISALLOW_COPY_AND_ASSIGN(HttpServer);
+};
+
+} // namespace test_servers
+} // namespace google_apis
+
+#endif // NET_TEST_EMBEDDED_TEST_SERVER_HTTP_SERVER_H_
diff --git a/net/test/embedded_test_server/http_server_unittest.cc b/net/test/embedded_test_server/http_server_unittest.cc
new file mode 100644
index 0000000..d10622c
--- /dev/null
+++ b/net/test/embedded_test_server/http_server_unittest.cc
@@ -0,0 +1,223 @@
+// Copyright (c) 2012 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 "net/test/embedded_test_server/http_server.h"
+
+#include "base/stringprintf.h"
+#include "base/threading/thread.h"
+#include "net/http/http_response_headers.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
+#include "net/url_request/url_fetcher.h"
+#include "net/url_request/url_fetcher_delegate.h"
+#include "net/url_request/url_request_test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace google_apis {
+namespace test_server {
+
+namespace {
+
+// Gets the content from the given URLFetcher.
+std::string GetContentFromFetcher(const net::URLFetcher& fetcher) {
+ std::string result;
+ const bool success = fetcher.GetResponseAsString(&result);
+ EXPECT_TRUE(success);
+ return result;
+}
+
+// Gets the content type from the given URLFetcher.
+std::string GetContentTypeFromFetcher(const net::URLFetcher& fetcher) {
+ const net::HttpResponseHeaders* headers = fetcher.GetResponseHeaders();
+ if (headers) {
+ std::string content_type;
+ if (headers->GetMimeType(&content_type))
+ return content_type;
+ }
+ return std::string();
+}
+
+} // namespace
+
+class HttpServerTest : public testing::Test,
+ public net::URLFetcherDelegate {
+ public:
+ HttpServerTest()
+ : num_responses_received_(0),
+ num_responses_expected_(0),
+ io_thread_("io_thread") {
+ }
+
+ virtual void SetUp() OVERRIDE {
+ base::Thread::Options thread_options;
+ thread_options.message_loop_type = MessageLoop::TYPE_IO;
+ ASSERT_TRUE(io_thread_.StartWithOptions(thread_options));
+
+ request_context_getter_ = new net::TestURLRequestContextGetter(
+ io_thread_.message_loop_proxy());
+
+ server_.reset(new HttpServer(io_thread_.message_loop_proxy()));
+ ASSERT_TRUE(server_->InitializeAndWaitUntilReady());
+ }
+
+ virtual void TearDown() OVERRIDE {
+ ASSERT_TRUE(server_->ShutdownAndWaitUntilComplete());
+ }
+
+ // net::URLFetcherDelegate override.
+ virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE {
+ ++num_responses_received_;
+ if (num_responses_received_ == num_responses_expected_)
+ MessageLoop::current()->Quit();
+ }
+
+ // Waits until the specified number of responses are received.
+ void WaitForResponses(int num_responses) {
+ num_responses_received_ = 0;
+ num_responses_expected_ = num_responses;
+ // Will be terminated in OnURLFetchComplete().
+ MessageLoop::current()->Run();
+ }
+
+ // Handles |request| sent to |path| and returns the response per |content|,
+ // |content type|, and |code|. Saves the request URL for verification.
+ scoped_ptr<HttpResponse> HandleRequest(const std::string& path,
+ const std::string& content,
+ const std::string& content_type,
+ ResponseCode code,
+ const HttpRequest& request) {
+ request_relative_url_ = request.relative_url;
+
+ GURL absolute_url = server_->GetURL(request.relative_url);
+ if (absolute_url.path() == path) {
+ scoped_ptr<HttpResponse> http_response(new HttpResponse);
+ http_response->set_code(code);
+ http_response->set_content(content);
+ http_response->set_content_type(content_type);
+ return http_response.Pass();
+ }
+
+ return scoped_ptr<HttpResponse>();
+ }
+
+ protected:
+ int num_responses_received_;
+ int num_responses_expected_;
+ std::string request_relative_url_;
+ base::Thread io_thread_;
+ scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_;
+ scoped_ptr<HttpServer> server_;
+};
+
+TEST_F(HttpServerTest, GetBaseURL) {
+ EXPECT_EQ(base::StringPrintf("http://127.0.0.1:%d/", server_->port()),
+ server_->base_url().spec());
+}
+
+TEST_F(HttpServerTest, GetURL) {
+ EXPECT_EQ(base::StringPrintf("http://127.0.0.1:%d/path?query=foo",
+ server_->port()),
+ server_->GetURL("/path?query=foo").spec());
+}
+
+TEST_F(HttpServerTest, RegisterRequestHandler) {
+ server_->RegisterRequestHandler(base::Bind(&HttpServerTest::HandleRequest,
+ base::Unretained(this),
+ "/test",
+ "<b>Worked!</b>",
+ "text/html",
+ SUCCESS));
+
+ scoped_ptr<net::URLFetcher> fetcher(
+ net::URLFetcher::Create(server_->GetURL("/test?q=foo"),
+ net::URLFetcher::GET,
+ this));
+ fetcher->SetRequestContext(request_context_getter_.get());
+ fetcher->Start();
+ WaitForResponses(1);
+
+ EXPECT_EQ(net::URLRequestStatus::SUCCESS, fetcher->GetStatus().status());
+ EXPECT_EQ(SUCCESS, fetcher->GetResponseCode());
+ EXPECT_EQ("<b>Worked!</b>", GetContentFromFetcher(*fetcher));
+ EXPECT_EQ("text/html", GetContentTypeFromFetcher(*fetcher));
+
+ EXPECT_EQ("/test?q=foo", request_relative_url_);
+}
+
+TEST_F(HttpServerTest, DefaultNotFoundResponse) {
+ scoped_ptr<net::URLFetcher> fetcher(
+ net::URLFetcher::Create(server_->GetURL("/non-existent"),
+ net::URLFetcher::GET,
+ this));
+ fetcher->SetRequestContext(request_context_getter_.get());
+
+ fetcher->Start();
+ WaitForResponses(1);
+ EXPECT_EQ(net::URLRequestStatus::SUCCESS, fetcher->GetStatus().status());
+ EXPECT_EQ(NOT_FOUND, fetcher->GetResponseCode());
+}
+
+TEST_F(HttpServerTest, ConcurrentFetches) {
+ server_->RegisterRequestHandler(
+ base::Bind(&HttpServerTest::HandleRequest,
+ base::Unretained(this),
+ "/test1",
+ "Raspberry chocolate",
+ "text/html",
+ SUCCESS));
+ server_->RegisterRequestHandler(
+ base::Bind(&HttpServerTest::HandleRequest,
+ base::Unretained(this),
+ "/test2",
+ "Vanilla chocolate",
+ "text/html",
+ SUCCESS));
+ server_->RegisterRequestHandler(
+ base::Bind(&HttpServerTest::HandleRequest,
+ base::Unretained(this),
+ "/test3",
+ "No chocolates",
+ "text/plain",
+ NOT_FOUND));
+
+ scoped_ptr<net::URLFetcher> fetcher1 = scoped_ptr<net::URLFetcher>(
+ net::URLFetcher::Create(server_->GetURL("/test1"),
+ net::URLFetcher::GET,
+ this));
+ fetcher1->SetRequestContext(request_context_getter_.get());
+ scoped_ptr<net::URLFetcher> fetcher2 = scoped_ptr<net::URLFetcher>(
+ net::URLFetcher::Create(server_->GetURL("/test2"),
+ net::URLFetcher::GET,
+ this));
+ fetcher2->SetRequestContext(request_context_getter_.get());
+ scoped_ptr<net::URLFetcher> fetcher3 = scoped_ptr<net::URLFetcher>(
+ net::URLFetcher::Create(server_->GetURL("/test3"),
+ net::URLFetcher::GET,
+ this));
+ fetcher3->SetRequestContext(request_context_getter_.get());
+
+ // Fetch the three URLs concurrently.
+ fetcher1->Start();
+ fetcher2->Start();
+ fetcher3->Start();
+ WaitForResponses(3);
+
+ EXPECT_EQ(net::URLRequestStatus::SUCCESS, fetcher1->GetStatus().status());
+ EXPECT_EQ(SUCCESS, fetcher1->GetResponseCode());
+ EXPECT_EQ("Raspberry chocolate", GetContentFromFetcher(*fetcher1));
+ EXPECT_EQ("text/html", GetContentTypeFromFetcher(*fetcher1));
+
+ EXPECT_EQ(net::URLRequestStatus::SUCCESS, fetcher2->GetStatus().status());
+ EXPECT_EQ(SUCCESS, fetcher2->GetResponseCode());
+ EXPECT_EQ("Vanilla chocolate", GetContentFromFetcher(*fetcher2));
+ EXPECT_EQ("text/html", GetContentTypeFromFetcher(*fetcher2));
+
+ EXPECT_EQ(net::URLRequestStatus::SUCCESS, fetcher3->GetStatus().status());
+ EXPECT_EQ(NOT_FOUND, fetcher3->GetResponseCode());
+ EXPECT_EQ("No chocolates", GetContentFromFetcher(*fetcher3));
+ EXPECT_EQ("text/plain", GetContentTypeFromFetcher(*fetcher3));
+}
+
+} // namespace test_server
+} // namespace google_apis