// 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 <numeric> #include <string> #include <vector> #include <base/values.h> #include <brillo/bind_lambda.h> #include <brillo/http/http_transport_fake.h> #include <brillo/http/http_utils.h> #include <brillo/mime_utils.h> #include <brillo/strings/string_utils.h> #include <brillo/url_utils.h> #include <gtest/gtest.h> namespace brillo { namespace http { 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)); } // 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(), brillo::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<uint8_t> custom_data{0xFF, 0x00, 0x80, 0x40, 0xC0, 0x7F}; auto response = http::SendRequestAndBlock(request_type::kPost, kEchoUrl, custom_data.data(), custom_data.size(), brillo::mime::application::kOctet_stream, {}, transport, nullptr); EXPECT_TRUE(response->IsSuccessful()); EXPECT_EQ(brillo::mime::application::kOctet_stream, response->GetContentType()); EXPECT_EQ(custom_data, response->ExtractData()); } TEST(HttpUtils, SendRequestAsync_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<uint8_t> custom_data{0xFF, 0x00, 0x80, 0x40, 0xC0, 0x7F}; auto success_callback = [&custom_data](RequestID /* id */, std::unique_ptr<http::Response> response) { EXPECT_TRUE(response->IsSuccessful()); EXPECT_EQ(brillo::mime::application::kOctet_stream, response->GetContentType()); EXPECT_EQ(custom_data, response->ExtractData()); }; auto error_callback = [](RequestID /* id */, const Error* /* error */) { FAIL() << "This callback shouldn't have been called"; }; http::SendRequest(request_type::kPost, kEchoUrl, custom_data.data(), custom_data.size(), brillo::mime::application::kOctet_stream, {}, transport, base::Bind(success_callback), base::Bind(error_callback)); } 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<uint8_t> custom_data{0xFF, 0x00, 0x80, 0x40, 0xC0, 0x7F}; // Check the correct HTTP method used. auto response = http::SendRequestAndBlock(request_type::kPost, kMethodEchoUrl, custom_data.data(), custom_data.size(), brillo::mime::application::kOctet_stream, {}, transport, nullptr); EXPECT_TRUE(response->IsSuccessful()); EXPECT_EQ(brillo::mime::text::kPlain, response->GetContentType()); EXPECT_EQ(request_type::kPost, response->ExtractDataAsString()); } TEST(HttpUtils, SendRequest_Get) { std::shared_ptr<fake::Transport> transport(new fake::Transport); transport->AddHandler(kMethodEchoUrl, "*", base::Bind(EchoMethodHandler)); auto response = http::SendRequestAndBlock(request_type::kGet, kMethodEchoUrl, nullptr, 0, std::string{}, {}, transport, nullptr); EXPECT_TRUE(response->IsSuccessful()); EXPECT_EQ(brillo::mime::text::kPlain, response->GetContentType()); EXPECT_EQ(request_type::kGet, response->ExtractDataAsString()); } TEST(HttpUtils, SendRequest_Put) { std::shared_ptr<fake::Transport> transport(new fake::Transport); transport->AddHandler(kMethodEchoUrl, "*", base::Bind(EchoMethodHandler)); auto response = http::SendRequestAndBlock(request_type::kPut, kMethodEchoUrl, nullptr, 0, std::string{}, {}, transport, nullptr); EXPECT_TRUE(response->IsSuccessful()); EXPECT_EQ(brillo::mime::text::kPlain, response->GetContentType()); EXPECT_EQ(request_type::kPut, response->ExtractDataAsString()); } TEST(HttpUtils, SendRequest_NotFound) { std::shared_ptr<fake::Transport> transport(new fake::Transport); // Test failed response (URL not found). auto response = http::SendRequestWithNoDataAndBlock( request_type::kGet, "http://blah.com", {}, transport, nullptr); EXPECT_FALSE(response->IsSuccessful()); EXPECT_EQ(status_code::NotFound, response->GetStatusCode()); } TEST(HttpUtils, SendRequestAsync_NotFound) { std::shared_ptr<fake::Transport> transport(new fake::Transport); // Test failed response (URL not found). auto success_callback = [](RequestID /* request_id */, std::unique_ptr<http::Response> response) { EXPECT_FALSE(response->IsSuccessful()); EXPECT_EQ(status_code::NotFound, response->GetStatusCode()); }; auto error_callback = [](RequestID /* request_id */, const Error* /* error */) { FAIL() << "This callback shouldn't have been called"; }; http::SendRequestWithNoData(request_type::kGet, "http://blah.com", {}, transport, base::Bind(success_callback), base::Bind(error_callback)); } 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 (const 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::SendRequestAndBlock( request_type::kPost, json_echo_url, "abcd", 4, brillo::mime::application::kOctet_stream, { {request_header::kCookie, "flavor=vanilla"}, {request_header::kIfMatch, "*"}, }, transport, nullptr); EXPECT_TRUE(response->IsSuccessful()); EXPECT_EQ(brillo::mime::application::kJson, brillo::mime::RemoveParameters(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(brillo::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"), brillo::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() actually does the GET request auto response = http::GetAndBlock(kMethodEchoUrl, {}, transport, nullptr); EXPECT_TRUE(response->IsSuccessful()); EXPECT_EQ(brillo::mime::text::kPlain, response->GetContentType()); EXPECT_EQ(request_type::kGet, response->ExtractDataAsString()); for (std::string data : {"blah", "some data", ""}) { std::string url = brillo::url::AppendQueryParam(kFakeUrl, "test", data); response = http::GetAndBlock(url, {}, transport, nullptr); EXPECT_EQ(data, response->ExtractDataAsString()); } } 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", brillo::mime::text::kPlain); }; std::shared_ptr<fake::Transport> transport(new fake::Transport); transport->AddHandler(kFakeUrl, request_type::kHead, base::Bind(HeadHandler)); auto response = http::HeadAndBlock(kFakeUrl, transport, nullptr); EXPECT_TRUE(response->IsSuccessful()); EXPECT_EQ(brillo::mime::text::kPlain, response->GetContentType()); EXPECT_EQ("", response->ExtractDataAsString()); // 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(brillo::mime::application::kOctet_stream, request.GetHeader(request_header::kContentType)); const 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, "", brillo::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<uint8_t> data(256); std::iota(data.begin(), data.end(), 0); auto response = http::PostBinaryAndBlock(kFakeUrl, data.data(), data.size(), mime::application::kOctet_stream, {}, transport, nullptr); EXPECT_TRUE(response->IsSuccessful()); } 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(), std::stoul(request.GetHeader(request_header::kContentLength))); EXPECT_EQ(brillo::mime::text::kPlain, request.GetHeader(request_header::kContentType)); response->ReplyText(status_code::Ok, request.GetDataAsString(), brillo::mime::text::kPlain); }; std::shared_ptr<fake::Transport> transport(new fake::Transport); transport->AddHandler(kFakeUrl, request_type::kPost, base::Bind(PostHandler)); auto response = http::PostTextAndBlock(kFakeUrl, fake_data, brillo::mime::text::kPlain, {}, transport, nullptr); EXPECT_TRUE(response->IsSuccessful()); EXPECT_EQ(brillo::mime::text::kPlain, response->GetContentType()); EXPECT_EQ(fake_data, response->ExtractDataAsString()); } TEST(HttpUtils, PostFormData) { std::shared_ptr<fake::Transport> transport(new fake::Transport); transport->AddHandler( kFakeUrl, request_type::kPost, base::Bind(EchoDataHandler)); auto response = http::PostFormDataAndBlock( kFakeUrl, { {"key", "value"}, {"field", "field value"}, }, {}, transport, nullptr); EXPECT_TRUE(response->IsSuccessful()); EXPECT_EQ(brillo::mime::application::kWwwFormUrlEncoded, response->GetContentType()); EXPECT_EQ("key=value&field=field+value", response->ExtractDataAsString()); } TEST(HttpUtils, PostMultipartFormData) { std::shared_ptr<fake::Transport> transport(new fake::Transport); transport->AddHandler( kFakeUrl, request_type::kPost, base::Bind(EchoDataHandler)); std::unique_ptr<FormData> form_data{new FormData{"boundary123"}}; form_data->AddTextField("key1", "value1"); form_data->AddTextField("key2", "value2"); std::string expected_content_type = form_data->GetContentType(); auto response = http::PostFormDataAndBlock( kFakeUrl, std::move(form_data), {}, transport, nullptr); EXPECT_TRUE(response->IsSuccessful()); EXPECT_EQ(expected_content_type, response->GetContentType()); const char expected_value[] = "--boundary123\r\n" "Content-Disposition: form-data; name=\"key1\"\r\n" "\r\n" "value1\r\n" "--boundary123\r\n" "Content-Disposition: form-data; name=\"key2\"\r\n" "\r\n" "value2\r\n" "--boundary123--"; EXPECT_EQ(expected_value, response->ExtractDataAsString()); } TEST(HttpUtils, PostPatchJson) { auto JsonHandler = [](const fake::ServerRequest& request, fake::ServerResponse* response) { auto mime_type = brillo::mime::RemoveParameters( request.GetHeader(request_header::kContentType)); EXPECT_EQ(brillo::mime::application::kJson, mime_type); response->ReplyJson( status_code::Ok, { {"method", request.GetMethod()}, {"data", request.GetDataAsString()}, }); }; 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::PostJsonAndBlock(kFakeUrl, &json, {}, transport, nullptr); 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::PatchJsonAndBlock(kFakeUrl, &json, {}, transport, nullptr); 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) { int status_code = std::stoi(request.GetFormField("code")); response->ReplyJson(status_code, {{"data", request.GetFormField("value")}}); }; 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 = brillo::string_utils::SplitAtFirst(item, ";"); auto response = http::PostFormDataAndBlock( kFakeUrl, { {"code", pair.first}, {"value", pair.second}, }, {}, transport, nullptr); 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, brillo::string_utils::ToString(code)); EXPECT_EQ(pair.second, value); } // Test invalid (non-JSON) response. auto response = http::GetAndBlock("http://bad.url", {}, transport, nullptr); EXPECT_EQ(status_code::NotFound, response->GetStatusCode()); EXPECT_EQ(brillo::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); } TEST(HttpUtils, SendRequest_Failure) { std::shared_ptr<fake::Transport> transport(new fake::Transport); transport->AddHandler(kMethodEchoUrl, "*", base::Bind(EchoMethodHandler)); ErrorPtr error; Error::AddTo(&error, FROM_HERE, "test_domain", "test_code", "Test message"); transport->SetCreateConnectionError(std::move(error)); error.reset(); // Just to make sure it is empty... auto response = http::SendRequestWithNoDataAndBlock( request_type::kGet, "http://blah.com", {}, transport, &error); EXPECT_EQ(nullptr, response.get()); EXPECT_EQ("test_domain", error->GetDomain()); EXPECT_EQ("test_code", error->GetCode()); EXPECT_EQ("Test message", error->GetMessage()); } TEST(HttpUtils, SendRequestAsync_Failure) { std::shared_ptr<fake::Transport> transport(new fake::Transport); transport->AddHandler(kMethodEchoUrl, "*", base::Bind(EchoMethodHandler)); ErrorPtr error; Error::AddTo(&error, FROM_HERE, "test_domain", "test_code", "Test message"); transport->SetCreateConnectionError(std::move(error)); auto success_callback = [](RequestID /* request_id */, std::unique_ptr<http::Response> /* response */) { FAIL() << "This callback shouldn't have been called"; }; auto error_callback = [](RequestID /* request_id */, const Error* error) { EXPECT_EQ("test_domain", error->GetDomain()); EXPECT_EQ("test_code", error->GetCode()); EXPECT_EQ("Test message", error->GetMessage()); }; http::SendRequestWithNoData(request_type::kGet, "http://blah.com", {}, transport, base::Bind(success_callback), base::Bind(error_callback)); } TEST(HttpUtils, GetCanonicalHeaderName) { EXPECT_EQ("Foo", GetCanonicalHeaderName("foo")); EXPECT_EQ("Bar", GetCanonicalHeaderName("BaR")); EXPECT_EQ("Baz", GetCanonicalHeaderName("BAZ")); EXPECT_EQ("Foo-Bar", GetCanonicalHeaderName("foo-bar")); EXPECT_EQ("Foo-Bar-Baz", GetCanonicalHeaderName("foo-Bar-BAZ")); EXPECT_EQ("Foo-Bar-Baz", GetCanonicalHeaderName("FOO-BAR-BAZ")); EXPECT_EQ("Foo-Bar-", GetCanonicalHeaderName("fOO-bAR-")); EXPECT_EQ("-Bar", GetCanonicalHeaderName("-bAR")); EXPECT_EQ("", GetCanonicalHeaderName("")); EXPECT_EQ("A-B-C", GetCanonicalHeaderName("a-B-c")); } } // namespace http } // namespace brillo