| // Copyright 2018 The Fuchsia 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 "src/cobalt/bin/utils/fuchsia_http_client.h" |
| |
| #include <lib/async/cpp/task.h> |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <memory> |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "lib/fidl/cpp/binding_set.h" |
| #include "lib/fidl/cpp/interface_request.h" |
| #include "lib/gtest/test_loop_fixture.h" |
| #include "lib/sys/cpp/testing/service_directory_provider.h" |
| #include "src/cobalt/bin/testing/fake_http_loader.h" |
| #include "src/lib/fsl/socket/strings.h" |
| #include "src/lib/fxl/command_line.h" |
| |
| namespace cobalt { |
| namespace utils { |
| |
| using lib::clearcut::HTTPRequest; |
| using lib::clearcut::HTTPResponse; |
| using lib::statusor::StatusOr; |
| using ::testing::HasSubstr; |
| |
| namespace { |
| |
| // Makes a string that is three times longer than the size of a SocketDrainer |
| // buffer so that we exercise our implementation of |
| // SocketDrainer::Client. |
| std::string MakeLongString() { |
| return std::string(64 * 1024, 'a') + std::string(64 * 1024, 'b') + std::string(64 * 1024, 'c'); |
| } |
| |
| std::vector<uint8_t> ToBytes(std::string str) { |
| return std::vector<uint8_t>(str.begin(), str.end()); |
| } |
| |
| } // namespace |
| |
| class FuchsiaHTTPClientTest : public ::gtest::TestLoopFixture { |
| public: |
| FuchsiaHTTPClientTest() |
| : ::gtest::TestLoopFixture(), |
| service_directory_provider_(new sys::testing::ServiceDirectoryProvider(dispatcher())), |
| http_client(new FuchsiaHTTPClient([this] { |
| fuchsia::net::http::LoaderSyncPtr loader; |
| service_directory_provider_->service_directory()->Connect(loader.NewRequest()); |
| return loader; |
| })) {} |
| |
| void SetUp() override { |
| loader_ = std::make_unique<FakeHTTPLoader>(dispatcher()); |
| service_directory_provider_->AddService(loader_->GetHandler()); |
| } |
| |
| void SetHttpResponse(const std::string& body, uint32_t status_code, |
| std::vector<fuchsia::net::http::Header> headers = {}) { |
| fuchsia::net::http::Response response; |
| if (!body.empty()) { |
| response.set_body(fsl::WriteStringToSocket(body)); |
| } |
| response.set_status_code(status_code); |
| response.set_headers(std::move(headers)); |
| loader_->SetResponse(std::move(response)); |
| } |
| |
| void SetHttpResponse(zx::socket body, uint32_t status_code, |
| std::vector<fuchsia::net::http::Header> headers = {}) { |
| fuchsia::net::http::Response response; |
| response.set_body(std::move(body)); |
| response.set_status_code(status_code); |
| response.set_headers(std::move(headers)); |
| loader_->SetResponse(std::move(response)); |
| } |
| |
| void SetNetworkErrorResponse(fuchsia::net::http::Error error) { |
| fuchsia::net::http::Response response; |
| response.set_final_url("https://www.example.com"); |
| response.set_error(error); |
| loader_->SetResponse(std::move(response)); |
| } |
| |
| void SetResponseDelay(zx::duration response_delay) { loader_->SetResponseDelay(response_delay); } |
| |
| // Invokes Post() in another thread (because Post() is not allowed to be |
| // invoked in the dispather's thread) and waits for Post() to complete and |
| // returns the value of Post(). |
| std::future<StatusOr<HTTPResponse>> PostString( |
| const std::string& body, std::chrono::milliseconds duration = std::chrono::seconds(1)) { |
| auto deadline = std::chrono::steady_clock::now() + duration; |
| |
| return std::async(std::launch::async, [this, body, deadline]() mutable { |
| return http_client->PostSync(HTTPRequest("http://www.test.com", body), deadline); |
| }); |
| } |
| |
| StatusOr<HTTPResponse> PostStringAndWait( |
| std::string request, std::chrono::milliseconds duration = std::chrono::seconds(1)) { |
| auto response_future = PostString("Request", duration); |
| auto start_time = std::chrono::steady_clock::now(); |
| while (response_future.wait_for(std::chrono::microseconds(1)) != std::future_status::ready) { |
| RunLoopUntilIdle(); |
| |
| if (std::chrono::steady_clock::now() - start_time > std::chrono::seconds(5)) { |
| return util::Status(util::StatusCode::DEADLINE_EXCEEDED, "Timed out while waiting!"); |
| } |
| } |
| // Note that here we are violating our own mandate to not wait on the |
| // future returned from Post() in the dispather's thread. The reason this |
| // is OK is that we inovked RunLoopUntilIdle() so we know that the future |
| // returned form Post() has already been prepared so that get() will not |
| // block. |
| return response_future.get(); |
| } |
| |
| // Prepares the FakeNetworkWrapper to return a response without a network |
| // error and then invokes PostString() and checks the returned HTTPResponse. |
| // |
| // |response_body_to_use| is the response body that the FakeNetworkWrapper will |
| // be asked to return. If this is empty then the |body| field will be empty. |
| // |
| // |http_response_code| is the code that the FakeNetworkWrapper will be asked |
| // to return. |
| // |
| // |include_response_headers| Should the FakeNetworkWrapper include any |
| // response headers. |
| void DoPostTest(std::string response_body_to_use, uint32_t http_response_code, |
| bool include_response_headers) { |
| std::vector<fuchsia::net::http::Header> response_headers_to_use; |
| if (include_response_headers) { |
| response_headers_to_use = { |
| fuchsia::net::http::Header{.name = ToBytes("name1"), .value = ToBytes("value1")}, |
| fuchsia::net::http::Header{.name = ToBytes("name2"), .value = ToBytes("value2")}}; |
| } |
| SetHttpResponse(response_body_to_use, http_response_code, response_headers_to_use); |
| auto response_or = PostStringAndWait("Request"); |
| ASSERT_TRUE(response_or.ok()); |
| auto response = response_or.ConsumeValueOrDie(); |
| EXPECT_EQ(response.response, response_body_to_use); |
| EXPECT_EQ(response.http_code, http_response_code); |
| if (include_response_headers) { |
| EXPECT_EQ(response.headers.size(), 2); |
| EXPECT_EQ(response.headers["name1"], "value1"); |
| EXPECT_EQ(response.headers["name2"], "value2"); |
| } else { |
| EXPECT_TRUE(response.headers.empty()); |
| } |
| } |
| |
| void CrashLoaderService() { |
| service_directory_provider_ = |
| std::make_unique<sys::testing::ServiceDirectoryProvider>(dispatcher()); |
| loader_->Unbind(); |
| } |
| |
| private: |
| std::unique_ptr<sys::testing::ServiceDirectoryProvider> service_directory_provider_; |
| std::unique_ptr<FakeHTTPLoader> loader_; |
| |
| public: |
| std::unique_ptr<FuchsiaHTTPClient> http_client; |
| }; |
| |
| TEST_F(FuchsiaHTTPClientTest, EmptyBodyNoHeaders) { DoPostTest("", 100, false); } |
| |
| TEST_F(FuchsiaHTTPClientTest, EmptyBodyWithHeaders) { DoPostTest("", 101, true); } |
| |
| TEST_F(FuchsiaHTTPClientTest, ShortBodyNoHeaders) { DoPostTest("Short response", 200, false); } |
| |
| TEST_F(FuchsiaHTTPClientTest, ShortBodyWithHeaders) { DoPostTest("Short response", 201, true); } |
| |
| TEST_F(FuchsiaHTTPClientTest, LongBodyNoHeaders) { DoPostTest(MakeLongString(), 202, false); } |
| |
| TEST_F(FuchsiaHTTPClientTest, LongBodyWithHeaders) { DoPostTest(MakeLongString(), 203, true); } |
| |
| TEST_F(FuchsiaHTTPClientTest, NetworkError) { |
| SetNetworkErrorResponse(fuchsia::net::http::Error::INTERNAL); |
| auto response_or = PostStringAndWait("Request"); |
| ASSERT_FALSE(response_or.ok()); |
| EXPECT_EQ(response_or.status().error_code(), util::StatusCode::INTERNAL); |
| EXPECT_THAT(response_or.status().error_message(), HasSubstr("Internal Error")); |
| } |
| |
| TEST_F(FuchsiaHTTPClientTest, TimeoutWithNoResponse) { |
| // Do not prepare any response. This causes the FakeNetworkWrapper to |
| // never return one so that we will get a timeout after 1 second. |
| |
| auto response_future = PostString("Request"); |
| |
| RunLoopUntilIdle(); |
| ASSERT_TRUE(response_future.valid()); |
| |
| while (response_future.wait_for(std::chrono::microseconds(1)) != std::future_status::ready) { |
| RunLoopUntilIdle(); |
| } |
| |
| ASSERT_EQ(std::future_status::ready, response_future.wait_for(std::chrono::microseconds(1))); |
| auto response_or = response_future.get(); |
| ASSERT_FALSE(response_or.ok()); |
| EXPECT_EQ(response_or.status().error_code(), util::StatusCode::DEADLINE_EXCEEDED); |
| } |
| |
| TEST_F(FuchsiaHTTPClientTest, MultipleSlowResponses) { |
| // Set delay for each response |
| SetResponseDelay(zx::sec(10)); |
| |
| // With a 10 second delay, requests should consistently time out. |
| SetHttpResponse("Body1", 200); |
| auto response_or = PostStringAndWait("Request"); |
| ASSERT_FALSE(response_or.ok()); |
| EXPECT_EQ(response_or.status().error_code(), util::StatusCode::DEADLINE_EXCEEDED); |
| |
| SetHttpResponse("Body2", 200); |
| response_or = PostStringAndWait("Request"); |
| ASSERT_FALSE(response_or.ok()); |
| EXPECT_EQ(response_or.status().error_code(), util::StatusCode::DEADLINE_EXCEEDED); |
| |
| SetHttpResponse("Body3", 200); |
| response_or = PostStringAndWait("Request"); |
| ASSERT_FALSE(response_or.ok()); |
| EXPECT_EQ(response_or.status().error_code(), util::StatusCode::DEADLINE_EXCEEDED); |
| |
| SetHttpResponse("Body4", 200); |
| response_or = PostStringAndWait("Request"); |
| ASSERT_FALSE(response_or.ok()); |
| EXPECT_EQ(response_or.status().error_code(), util::StatusCode::DEADLINE_EXCEEDED); |
| |
| SetHttpResponse("Body5", 200); |
| response_or = PostStringAndWait("Request"); |
| ASSERT_FALSE(response_or.ok()); |
| EXPECT_EQ(response_or.status().error_code(), util::StatusCode::DEADLINE_EXCEEDED); |
| |
| // With no delay, all should be fine |
| SetResponseDelay(zx::sec(0)); |
| DoPostTest("Body6", 200, false); |
| } |
| |
| TEST_F(FuchsiaHTTPClientTest, HandlesServiceCrash) { |
| DoPostTest("", 200, false); |
| |
| CrashLoaderService(); |
| |
| auto response_or = PostStringAndWait("Request"); |
| ASSERT_FALSE(response_or.ok()); |
| EXPECT_EQ(response_or.status().error_code(), util::StatusCode::UNAVAILABLE); |
| EXPECT_THAT(response_or.status().error_message(), HasSubstr("ZX_ERR_PEER_CLOSED")); |
| } |
| |
| TEST_F(FuchsiaHTTPClientTest, SlowSocket) { |
| zx::socket tx, rx; |
| zx::socket::create(ZX_SOCKET_STREAM, &tx, &rx); |
| SetHttpResponse(std::move(rx), 200); |
| |
| auto response_future = PostString("Test123", std::chrono::seconds(30)); |
| RunLoopUntilIdle(); |
| |
| for (int i = 0; i < 100; i++) { |
| std::vector<char> buffer(1024); |
| tx.write(0, buffer.data(), buffer.size(), nullptr); |
| RunLoopUntilIdle(); |
| zx::nanosleep(zx::deadline_after(zx::msec(1))); |
| } |
| tx.reset(); |
| |
| auto response_or = response_future.get(); |
| ASSERT_TRUE(response_or.ok()); |
| } |
| |
| TEST_F(FuchsiaHTTPClientTest, HungSocket) { |
| zx::socket tx, rx; |
| zx::socket::create(ZX_SOCKET_STREAM, &tx, &rx); |
| SetHttpResponse(std::move(rx), 200); |
| |
| // This will hang forever if the socket read waits for zx::time::infinite(). |
| auto response_or = PostStringAndWait("Test123", std::chrono::milliseconds(10)); |
| |
| ASSERT_FALSE(response_or.ok()); |
| EXPECT_EQ(response_or.status().error_code(), util::StatusCode::DEADLINE_EXCEEDED); |
| } |
| |
| } // namespace utils |
| } // namespace cobalt |