| Index: mojo/services/network/http_server_apptest.cc
|
| diff --git a/mojo/services/network/http_server_apptest.cc b/mojo/services/network/http_server_apptest.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..f6d35e0ab8a8ee5431ef2a6f569b17434484da75
|
| --- /dev/null
|
| +++ b/mojo/services/network/http_server_apptest.cc
|
| @@ -0,0 +1,481 @@
|
| +// Copyright 2015 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 "base/macros.h"
|
| +#include "base/memory/linked_ptr.h"
|
| +#include "base/memory/ref_counted.h"
|
| +#include "base/memory/scoped_ptr.h"
|
| +#include "base/run_loop.h"
|
| +#include "base/strings/string_util.h"
|
| +#include "mojo/application/application_test_base_chromium.h"
|
| +#include "mojo/common/data_pipe_utils.h"
|
| +#include "mojo/public/cpp/application/application_connection.h"
|
| +#include "mojo/public/cpp/application/application_impl.h"
|
| +#include "mojo/services/network/net_address_type_converters.h"
|
| +#include "mojo/services/network/public/interfaces/http_server.mojom.h"
|
| +#include "mojo/services/network/public/interfaces/network_service.mojom.h"
|
| +#include "net/base/io_buffer.h"
|
| +#include "net/base/net_errors.h"
|
| +#include "net/base/test_completion_callback.h"
|
| +#include "net/http/http_response_headers.h"
|
| +#include "net/http/http_util.h"
|
| +#include "net/socket/tcp_client_socket.h"
|
| +#include "testing/gtest/include/gtest/gtest.h"
|
| +
|
| +namespace mojo {
|
| +namespace {
|
| +
|
| +const int kMaxExpectedResponseLength = 2048;
|
| +
|
| +NetAddressPtr GetLocalHostWithAnyPort() {
|
| + NetAddressPtr addr(NetAddress::New());
|
| + addr->family = NET_ADDRESS_FAMILY_IPV4;
|
| + addr->ipv4 = NetAddressIPv4::New();
|
| + addr->ipv4->port = 0;
|
| + addr->ipv4->addr.resize(4);
|
| + addr->ipv4->addr[0] = 127;
|
| + addr->ipv4->addr[1] = 0;
|
| + addr->ipv4->addr[2] = 0;
|
| + addr->ipv4->addr[3] = 1;
|
| +
|
| + return addr.Pass();
|
| +}
|
| +
|
| +using TestHeaders = std::vector<std::pair<std::string, std::string>>;
|
| +
|
| +struct TestRequest {
|
| + std::string method;
|
| + std::string url;
|
| + TestHeaders headers;
|
| + scoped_ptr<std::string> body;
|
| +};
|
| +
|
| +struct TestResponse {
|
| + uint32_t status_code;
|
| + TestHeaders headers;
|
| + scoped_ptr<std::string> body;
|
| +};
|
| +
|
| +std::string MakeRequestMessage(const TestRequest& data) {
|
| + std::string message = data.method + " " + data.url + " HTTP/1.1\r\n";
|
| + for (const auto& item : data.headers)
|
| + message += item.first + ": " + item.second + "\r\n";
|
| + message += "\r\n";
|
| + if (data.body)
|
| + message += *data.body;
|
| +
|
| + return message;
|
| +}
|
| +
|
| +URLResponsePtr MakeResponseStruct(const TestResponse& data) {
|
| + URLResponsePtr response(URLResponse::New());
|
| + response->status_code = data.status_code;
|
| + response->headers.resize(data.headers.size());
|
| + size_t index = 0;
|
| + for (const auto& item : data.headers) {
|
| + HTTPHeaderPtr header(HTTPHeader::New());
|
| + header->name = item.first;
|
| + header->value = item.second;
|
| + response->headers[index++] = header.Pass();
|
| + }
|
| +
|
| + if (data.body) {
|
| + uint32_t num_bytes = static_cast<uint32_t>(data.body->size());
|
| + MojoCreateDataPipeOptions options;
|
| + options.struct_size = sizeof(MojoCreateDataPipeOptions);
|
| + options.flags = MOJO_CREATE_DATA_PIPE_OPTIONS_FLAG_NONE;
|
| + options.element_num_bytes = 1;
|
| + options.capacity_num_bytes = num_bytes;
|
| + DataPipe data_pipe(options);
|
| + response->body = data_pipe.consumer_handle.Pass();
|
| + MojoResult result =
|
| + WriteDataRaw(data_pipe.producer_handle.get(), data.body->data(),
|
| + &num_bytes, MOJO_WRITE_DATA_FLAG_ALL_OR_NONE);
|
| + EXPECT_EQ(MOJO_RESULT_OK, result);
|
| + }
|
| +
|
| + return response.Pass();
|
| +}
|
| +
|
| +void CheckHeaders(const TestHeaders& expected,
|
| + const Array<HTTPHeaderPtr>& headers) {
|
| + // The server impl fiddles with Content-Length and Content-Type. So we don't
|
| + // do a strict check here.
|
| + std::map<std::string, std::string> header_map;
|
| + for (size_t i = 0; i < headers.size(); ++i) {
|
| + std::string lower_name =
|
| + base::StringToLowerASCII(headers[i]->name.To<std::string>());
|
| + header_map[lower_name] = headers[i]->value;
|
| + }
|
| +
|
| + for (const auto& item : expected) {
|
| + std::string lower_name = base::StringToLowerASCII(item.first);
|
| + EXPECT_NE(header_map.end(), header_map.find(lower_name));
|
| + EXPECT_EQ(item.second, header_map[lower_name]);
|
| + }
|
| +}
|
| +
|
| +void CheckRequest(const TestRequest& expected, URLRequestPtr request) {
|
| + EXPECT_EQ(expected.method, request->method);
|
| + EXPECT_EQ(expected.url, request->url);
|
| + CheckHeaders(expected.headers, request->headers);
|
| + if (expected.body) {
|
| + EXPECT_EQ(1u, request->body.size());
|
| + std::string body;
|
| + common::BlockingCopyToString(request->body[0].Pass(), &body);
|
| + EXPECT_EQ(*expected.body, body);
|
| + } else {
|
| + EXPECT_EQ(0u, request->body.size());
|
| + }
|
| +}
|
| +
|
| +void CheckResponse(const TestResponse& expected, const std::string& response) {
|
| + int header_end =
|
| + net::HttpUtil::LocateEndOfHeaders(response.c_str(), response.size());
|
| + std::string assembled_headers =
|
| + net::HttpUtil::AssembleRawHeaders(response.c_str(), header_end);
|
| + scoped_refptr<net::HttpResponseHeaders> parsed_headers(
|
| + new net::HttpResponseHeaders(assembled_headers));
|
| + EXPECT_EQ(expected.status_code,
|
| + static_cast<uint32_t>(parsed_headers->response_code()));
|
| + for (const auto& item : expected.headers)
|
| + EXPECT_TRUE(parsed_headers->HasHeaderValue(item.first, item.second));
|
| +
|
| + if (expected.body) {
|
| + EXPECT_NE(-1, header_end);
|
| + std::string body(response, static_cast<size_t>(header_end));
|
| + EXPECT_EQ(*expected.body, body);
|
| + } else {
|
| + EXPECT_EQ(response.size(), static_cast<size_t>(header_end));
|
| + }
|
| +}
|
| +
|
| +class TestHttpClient {
|
| + public:
|
| + TestHttpClient() : connect_result_(net::OK) {}
|
| +
|
| + void Connect(const net::IPEndPoint& address) {
|
| + net::AddressList addresses(address);
|
| + net::NetLog::Source source;
|
| + socket_.reset(new net::TCPClientSocket(addresses, NULL, source));
|
| +
|
| + base::RunLoop run_loop;
|
| + connect_result_ = socket_->Connect(base::Bind(&TestHttpClient::OnConnect,
|
| + base::Unretained(this),
|
| + run_loop.QuitClosure()));
|
| + if (connect_result_ == net::ERR_IO_PENDING)
|
| + run_loop.Run();
|
| +
|
| + ASSERT_EQ(net::OK, connect_result_);
|
| + }
|
| +
|
| + void Send(const std::string& data) {
|
| + write_buffer_ = new net::DrainableIOBuffer(new net::StringIOBuffer(data),
|
| + data.length());
|
| + Write();
|
| + }
|
| +
|
| + // Note: This method determines the end of the response only by Content-Length
|
| + // and connection termination. Besides, it doesn't truncate at the end of the
|
| + // response, so |message| may return more data (e.g., part of the next
|
| + // response).
|
| + void ReadResponse(std::string* message) {
|
| + if (!Read(message, 1))
|
| + return;
|
| + while (!IsCompleteResponse(*message)) {
|
| + std::string chunk;
|
| + if (!Read(&chunk, 1))
|
| + return;
|
| + message->append(chunk);
|
| + }
|
| + return;
|
| + }
|
| +
|
| + private:
|
| + void OnConnect(const base::Closure& quit_loop, int result) {
|
| + connect_result_ = result;
|
| + quit_loop.Run();
|
| + }
|
| +
|
| + void Write() {
|
| + int result = socket_->Write(
|
| + write_buffer_.get(), write_buffer_->BytesRemaining(),
|
| + base::Bind(&TestHttpClient::OnWrite, base::Unretained(this)));
|
| + if (result != net::ERR_IO_PENDING)
|
| + OnWrite(result);
|
| + }
|
| +
|
| + void OnWrite(int result) {
|
| + ASSERT_GT(result, 0);
|
| + write_buffer_->DidConsume(result);
|
| + if (write_buffer_->BytesRemaining())
|
| + Write();
|
| + }
|
| +
|
| + bool Read(std::string* message, int expected_bytes) {
|
| + int total_bytes_received = 0;
|
| + message->clear();
|
| + while (total_bytes_received < expected_bytes) {
|
| + net::TestCompletionCallback callback;
|
| + ReadInternal(callback.callback());
|
| + int bytes_received = callback.WaitForResult();
|
| + if (bytes_received <= 0)
|
| + return false;
|
| +
|
| + total_bytes_received += bytes_received;
|
| + message->append(read_buffer_->data(), bytes_received);
|
| + }
|
| + return true;
|
| + }
|
| +
|
| + void ReadInternal(const net::CompletionCallback& callback) {
|
| + read_buffer_ = new net::IOBufferWithSize(kMaxExpectedResponseLength);
|
| + int result =
|
| + socket_->Read(read_buffer_.get(), kMaxExpectedResponseLength, callback);
|
| + if (result != net::ERR_IO_PENDING)
|
| + callback.Run(result);
|
| + }
|
| +
|
| + bool IsCompleteResponse(const std::string& response) {
|
| + // Check end of headers first.
|
| + int end_of_headers =
|
| + net::HttpUtil::LocateEndOfHeaders(response.data(), response.size());
|
| + if (end_of_headers < 0)
|
| + return false;
|
| +
|
| + // Return true if response has data equal to or more than content length.
|
| + int64 body_size = static_cast<int64>(response.size()) - end_of_headers;
|
| + DCHECK_LE(0, body_size);
|
| + scoped_refptr<net::HttpResponseHeaders> headers(
|
| + new net::HttpResponseHeaders(net::HttpUtil::AssembleRawHeaders(
|
| + response.data(), end_of_headers)));
|
| + return body_size >= headers->GetContentLength();
|
| + }
|
| +
|
| + scoped_refptr<net::IOBufferWithSize> read_buffer_;
|
| + scoped_refptr<net::DrainableIOBuffer> write_buffer_;
|
| + scoped_ptr<net::TCPClientSocket> socket_;
|
| + int connect_result_;
|
| +
|
| + DISALLOW_COPY_AND_ASSIGN(TestHttpClient);
|
| +};
|
| +
|
| +class HttpConnectionDelegateImpl : public HttpConnectionDelegate {
|
| + public:
|
| + struct PendingRequest {
|
| + URLRequestPtr request;
|
| + OnReceivedRequestCallback callback;
|
| + };
|
| +
|
| + HttpConnectionDelegateImpl(HttpConnectionPtr connection,
|
| + InterfaceRequest<HttpConnectionDelegate> request)
|
| + : connection_(connection.Pass()),
|
| + binding_(this, request.Pass()),
|
| + run_loop_(nullptr),
|
| + wait_for_request_count_(0) {}
|
| + ~HttpConnectionDelegateImpl() override {}
|
| +
|
| + // HttpConnectionDelegate implementation:
|
| + void OnReceivedRequest(URLRequestPtr request,
|
| + const OnReceivedRequestCallback& callback) override {
|
| + linked_ptr<PendingRequest> pending_request(new PendingRequest);
|
| + pending_request->request = request.Pass();
|
| + pending_request->callback = callback;
|
| + pending_requests_.push_back(pending_request);
|
| + if (run_loop_ && pending_requests_.size() >= wait_for_request_count_) {
|
| + wait_for_request_count_ = 0;
|
| + run_loop_->Quit();
|
| + }
|
| + }
|
| +
|
| + void OnReceivedWebSocketRequest(
|
| + URLRequestPtr request,
|
| + const OnReceivedWebSocketRequestCallback& callback) override {
|
| + NOTREACHED();
|
| + }
|
| +
|
| + void SendResponse(URLResponsePtr response) {
|
| + ASSERT_FALSE(pending_requests_.empty());
|
| + linked_ptr<PendingRequest> request = pending_requests_[0];
|
| + pending_requests_.erase(pending_requests_.begin());
|
| + request->callback.Run(response.Pass());
|
| + }
|
| +
|
| + void WaitForRequest(size_t count) {
|
| + DCHECK(!run_loop_);
|
| +
|
| + wait_for_request_count_ = count;
|
| + base::RunLoop run_loop;
|
| + run_loop_ = &run_loop;
|
| + run_loop.Run();
|
| + run_loop_ = nullptr;
|
| + }
|
| +
|
| + std::vector<linked_ptr<PendingRequest>>& pending_requests() {
|
| + return pending_requests_;
|
| + }
|
| +
|
| + private:
|
| + HttpConnectionPtr connection_;
|
| + Binding<HttpConnectionDelegate> binding_;
|
| + std::vector<linked_ptr<PendingRequest>> pending_requests_;
|
| + // Pointing to a stack-allocated RunLoop instance.
|
| + base::RunLoop* run_loop_;
|
| + size_t wait_for_request_count_;
|
| +
|
| + DISALLOW_COPY_AND_ASSIGN(HttpConnectionDelegateImpl);
|
| +};
|
| +
|
| +class HttpServerDelegateImpl : public HttpServerDelegate {
|
| + public:
|
| + explicit HttpServerDelegateImpl(HttpServerDelegatePtr* delegate_ptr)
|
| + : binding_(this, delegate_ptr),
|
| + run_loop_(nullptr),
|
| + wait_for_connection_count_(0) {}
|
| + ~HttpServerDelegateImpl() override {}
|
| +
|
| + // HttpServerDelegate implementation.
|
| + void OnConnected(HttpConnectionPtr connection,
|
| + InterfaceRequest<HttpConnectionDelegate> delegate) override {
|
| + connections_.push_back(make_linked_ptr(
|
| + new HttpConnectionDelegateImpl(connection.Pass(), delegate.Pass())));
|
| + if (run_loop_ && connections_.size() >= wait_for_connection_count_) {
|
| + wait_for_connection_count_ = 0;
|
| + run_loop_->Quit();
|
| + }
|
| + }
|
| +
|
| + void WaitForConnection(size_t count) {
|
| + DCHECK(!run_loop_);
|
| +
|
| + wait_for_connection_count_ = count;
|
| + base::RunLoop run_loop;
|
| + run_loop_ = &run_loop;
|
| + run_loop.Run();
|
| + run_loop_ = nullptr;
|
| + }
|
| +
|
| + std::vector<linked_ptr<HttpConnectionDelegateImpl>>& connections() {
|
| + return connections_;
|
| + }
|
| +
|
| + private:
|
| + Binding<HttpServerDelegate> binding_;
|
| + std::vector<linked_ptr<HttpConnectionDelegateImpl>> connections_;
|
| + // Pointing to a stack-allocated RunLoop instance.
|
| + base::RunLoop* run_loop_;
|
| + size_t wait_for_connection_count_;
|
| +
|
| + DISALLOW_COPY_AND_ASSIGN(HttpServerDelegateImpl);
|
| +};
|
| +
|
| +class HttpServerAppTest : public test::ApplicationTestBase {
|
| + public:
|
| + HttpServerAppTest() : message_loop_(base::MessageLoop::TYPE_IO) {}
|
| + ~HttpServerAppTest() override {}
|
| +
|
| + protected:
|
| + bool ShouldCreateDefaultRunLoop() override { return false; }
|
| +
|
| + void SetUp() override {
|
| + ApplicationTestBase::SetUp();
|
| +
|
| + ApplicationConnection* connection =
|
| + application_impl()->ConnectToApplication("mojo:network_service");
|
| + connection->ConnectToService(&network_service_);
|
| + }
|
| +
|
| + void CreateHttpServer(HttpServerDelegatePtr delegate,
|
| + NetAddressPtr* out_bound_to) {
|
| + network_service_->CreateHttpServer(
|
| + GetLocalHostWithAnyPort(), delegate.Pass(),
|
| + [out_bound_to](NetworkErrorPtr result, NetAddressPtr bound_to) {
|
| + ASSERT_EQ(net::OK, result->code);
|
| + EXPECT_NE(0u, bound_to->ipv4->port);
|
| + *out_bound_to = bound_to.Pass();
|
| + });
|
| + network_service_.WaitForIncomingMethodCall();
|
| + }
|
| +
|
| + NetworkServicePtr network_service_;
|
| +
|
| + private:
|
| + base::MessageLoop message_loop_;
|
| +
|
| + DISALLOW_COPY_AND_ASSIGN(HttpServerAppTest);
|
| +};
|
| +
|
| +} // namespace
|
| +
|
| +TEST_F(HttpServerAppTest, BasicHttpRequestResponse) {
|
| + NetAddressPtr bound_to;
|
| + HttpServerDelegatePtr server_delegate_ptr;
|
| + HttpServerDelegateImpl server_delegate_impl(&server_delegate_ptr);
|
| + CreateHttpServer(server_delegate_ptr.Pass(), &bound_to);
|
| +
|
| + TestHttpClient client;
|
| + client.Connect(bound_to.To<net::IPEndPoint>());
|
| +
|
| + server_delegate_impl.WaitForConnection(1);
|
| + HttpConnectionDelegateImpl& connection =
|
| + *server_delegate_impl.connections()[0];
|
| +
|
| + TestRequest request_data = {"HEAD", "/test", {{"Hello", "World"}}, nullptr};
|
| + client.Send(MakeRequestMessage(request_data));
|
| +
|
| + connection.WaitForRequest(1);
|
| +
|
| + CheckRequest(request_data, connection.pending_requests()[0]->request.Pass());
|
| +
|
| + TestResponse response_data = {200, {{"Content-Length", "4"}}, nullptr};
|
| + connection.SendResponse(MakeResponseStruct(response_data));
|
| + // This causes the underlying TCP connection to be closed. The client can
|
| + // determine the end of the response based on that.
|
| + server_delegate_impl.connections().clear();
|
| +
|
| + std::string response_message;
|
| + client.ReadResponse(&response_message);
|
| +
|
| + CheckResponse(response_data, response_message);
|
| +}
|
| +
|
| +TEST_F(HttpServerAppTest, HttpRequestResponseWithBody) {
|
| + NetAddressPtr bound_to;
|
| + HttpServerDelegatePtr server_delegate_ptr;
|
| + HttpServerDelegateImpl server_delegate_impl(&server_delegate_ptr);
|
| + CreateHttpServer(server_delegate_ptr.Pass(), &bound_to);
|
| +
|
| + TestHttpClient client;
|
| + client.Connect(bound_to.To<net::IPEndPoint>());
|
| +
|
| + server_delegate_impl.WaitForConnection(1);
|
| + HttpConnectionDelegateImpl& connection =
|
| + *server_delegate_impl.connections()[0];
|
| +
|
| + TestRequest request_data = {
|
| + "Post",
|
| + "/test",
|
| + {{"Hello", "World"},
|
| + {"Content-Length", "23"},
|
| + {"Content-Type", "text/plain"}},
|
| + make_scoped_ptr(new std::string("This is a test request!"))};
|
| + client.Send(MakeRequestMessage(request_data));
|
| +
|
| + connection.WaitForRequest(1);
|
| +
|
| + CheckRequest(request_data, connection.pending_requests()[0]->request.Pass());
|
| +
|
| + TestResponse response_data = {
|
| + 200,
|
| + {{"Content-Length", "26"}},
|
| + make_scoped_ptr(new std::string("This is a test response..."))};
|
| + connection.SendResponse(MakeResponseStruct(response_data));
|
| +
|
| + std::string response_message;
|
| + client.ReadResponse(&response_message);
|
| +
|
| + CheckResponse(response_data, response_message);
|
| +}
|
| +
|
| +} // namespace mojo
|
|
|