// Copyright Epic Games, Inc. All Rights Reserved. #include #include #if ZEN_WITH_TESTS # include # include # include # include # include # include "servers/httpasio.h" # include # include ZEN_THIRD_PARTY_INCLUDES_START # include ZEN_THIRD_PARTY_INCLUDES_END namespace zen { using namespace std::literals; ////////////////////////////////////////////////////////////////////////// // Reusable test service for async client tests class AsyncHttpClientTestService : public HttpService { public: AsyncHttpClientTestService() { m_Router.RegisterRoute( "hello", [](HttpRouterRequest& Req) { Req.ServerRequest().WriteResponse(HttpResponseCode::OK, HttpContentType::kText, "hello world"); }, HttpVerb::kGet); m_Router.RegisterRoute( "echo", [](HttpRouterRequest& Req) { HttpServerRequest& HttpReq = Req.ServerRequest(); IoBuffer Body = HttpReq.ReadPayload(); HttpContentType CT = HttpReq.RequestContentType(); HttpReq.WriteResponse(HttpResponseCode::OK, CT, Body); }, HttpVerb::kPost | HttpVerb::kPut); m_Router.RegisterRoute( "echo/method", [](HttpRouterRequest& Req) { HttpServerRequest& HttpReq = Req.ServerRequest(); std::string_view Method = ToString(HttpReq.RequestVerb()); HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, Method); }, HttpVerb::kGet | HttpVerb::kPost | HttpVerb::kPut | HttpVerb::kDelete | HttpVerb::kHead); m_Router.RegisterRoute( "nocontent", [](HttpRouterRequest& Req) { Req.ServerRequest().WriteResponse(HttpResponseCode::NoContent); }, HttpVerb::kGet | HttpVerb::kPost | HttpVerb::kPut | HttpVerb::kDelete); m_Router.RegisterRoute( "json", [](HttpRouterRequest& Req) { Req.ServerRequest().WriteResponse(HttpResponseCode::OK, HttpContentType::kJSON, "{\"ok\":true}"); }, HttpVerb::kGet); } virtual const char* BaseUri() const override { return "/api/async-test/"; } virtual void HandleRequest(HttpServerRequest& Request) override { m_Router.HandleRequest(Request); } private: HttpRequestRouter m_Router; }; ////////////////////////////////////////////////////////////////////////// struct AsyncTestServerFixture { AsyncHttpClientTestService TestService; ScopedTemporaryDirectory TmpDir; Ref Server; std::thread ServerThread; int Port = -1; AsyncTestServerFixture() { Server = CreateHttpAsioServer(AsioConfig{}); Port = Server->Initialize(0, TmpDir.Path()); ZEN_ASSERT(Port != -1); Server->RegisterService(TestService); ServerThread = std::thread([this]() { Server->Run(false); }); } ~AsyncTestServerFixture() { Server->RequestExit(); if (ServerThread.joinable()) { ServerThread.join(); } Server->Close(); } AsyncHttpClient MakeClient(HttpClientSettings Settings = {}) { return AsyncHttpClient(fmt::format("127.0.0.1:{}", Port), Settings); } AsyncHttpClient MakeClient(asio::io_context& IoContext, HttpClientSettings Settings = {}) { return AsyncHttpClient(fmt::format("127.0.0.1:{}", Port), IoContext, Settings); } }; ////////////////////////////////////////////////////////////////////////// // Tests TEST_SUITE_BEGIN("http.asynchttpclient"); TEST_CASE("asynchttpclient.future.verbs") { AsyncTestServerFixture Fixture; AsyncHttpClient Client = Fixture.MakeClient(); SUBCASE("GET returns 200 with expected body") { auto Future = Client.Get("/api/async-test/echo/method"); auto Resp = Future.get(); CHECK(Resp.IsSuccess()); CHECK_EQ(Resp.AsText(), "GET"); } SUBCASE("POST dispatches correctly") { auto Future = Client.Post("/api/async-test/echo/method"); auto Resp = Future.get(); CHECK(Resp.IsSuccess()); CHECK_EQ(Resp.AsText(), "POST"); } SUBCASE("PUT dispatches correctly") { auto Future = Client.Put("/api/async-test/echo/method"); auto Resp = Future.get(); CHECK(Resp.IsSuccess()); CHECK_EQ(Resp.AsText(), "PUT"); } SUBCASE("DELETE dispatches correctly") { auto Future = Client.Delete("/api/async-test/echo/method"); auto Resp = Future.get(); CHECK(Resp.IsSuccess()); CHECK_EQ(Resp.AsText(), "DELETE"); } SUBCASE("HEAD returns 200 with empty body") { auto Future = Client.Head("/api/async-test/echo/method"); auto Resp = Future.get(); CHECK(Resp.IsSuccess()); CHECK_EQ(Resp.AsText(), ""sv); } } TEST_CASE("asynchttpclient.future.get") { AsyncTestServerFixture Fixture; AsyncHttpClient Client = Fixture.MakeClient(); SUBCASE("simple GET with text response") { auto Future = Client.Get("/api/async-test/hello"); auto Resp = Future.get(); CHECK(Resp.IsSuccess()); CHECK_EQ(Resp.StatusCode, HttpResponseCode::OK); CHECK_EQ(Resp.AsText(), "hello world"); } SUBCASE("GET returning JSON") { auto Future = Client.Get("/api/async-test/json"); auto Resp = Future.get(); CHECK(Resp.IsSuccess()); CHECK_EQ(Resp.AsText(), "{\"ok\":true}"); } SUBCASE("GET 204 NoContent") { auto Future = Client.Get("/api/async-test/nocontent"); auto Resp = Future.get(); CHECK(Resp.IsSuccess()); CHECK_EQ(Resp.StatusCode, HttpResponseCode::NoContent); } } TEST_CASE("asynchttpclient.future.post.with.payload") { AsyncTestServerFixture Fixture; AsyncHttpClient Client = Fixture.MakeClient(); std::string_view PayloadStr = "async payload data"; IoBuffer Payload(IoBuffer::Clone, PayloadStr.data(), PayloadStr.size()); Payload.SetContentType(ZenContentType::kText); auto Future = Client.Post("/api/async-test/echo", Payload); auto Resp = Future.get(); CHECK(Resp.IsSuccess()); CHECK_EQ(Resp.AsText(), "async payload data"); } TEST_CASE("asynchttpclient.future.put.with.payload") { AsyncTestServerFixture Fixture; AsyncHttpClient Client = Fixture.MakeClient(); std::string_view PutStr = "put payload"; IoBuffer Payload(IoBuffer::Clone, PutStr.data(), PutStr.size()); Payload.SetContentType(ZenContentType::kText); auto Future = Client.Put("/api/async-test/echo", Payload); auto Resp = Future.get(); CHECK(Resp.IsSuccess()); CHECK_EQ(Resp.AsText(), "put payload"); } TEST_CASE("asynchttpclient.callback") { AsyncTestServerFixture Fixture; AsyncHttpClient Client = Fixture.MakeClient(); std::promise Promise; auto Future = Promise.get_future(); Client.AsyncGet("/api/async-test/hello", [&Promise](HttpClient::Response Resp) { Promise.set_value(std::move(Resp)); }); auto Resp = Future.get(); CHECK(Resp.IsSuccess()); CHECK_EQ(Resp.AsText(), "hello world"); } TEST_CASE("asynchttpclient.concurrent.requests") { AsyncTestServerFixture Fixture; AsyncHttpClient Client = Fixture.MakeClient(); // Fire multiple requests concurrently auto Future1 = Client.Get("/api/async-test/hello"); auto Future2 = Client.Get("/api/async-test/json"); auto Future3 = Client.Post("/api/async-test/echo/method"); auto Future4 = Client.Delete("/api/async-test/echo/method"); auto Resp1 = Future1.get(); auto Resp2 = Future2.get(); auto Resp3 = Future3.get(); auto Resp4 = Future4.get(); CHECK(Resp1.IsSuccess()); CHECK_EQ(Resp1.AsText(), "hello world"); CHECK(Resp2.IsSuccess()); CHECK_EQ(Resp2.AsText(), "{\"ok\":true}"); CHECK(Resp3.IsSuccess()); CHECK_EQ(Resp3.AsText(), "POST"); CHECK(Resp4.IsSuccess()); CHECK_EQ(Resp4.AsText(), "DELETE"); } TEST_CASE("asynchttpclient.external.io_context") { AsyncTestServerFixture Fixture; asio::io_context IoContext; auto WorkGuard = asio::make_work_guard(IoContext); std::thread IoThread([&IoContext]() { IoContext.run(); }); { AsyncHttpClient Client = Fixture.MakeClient(IoContext); auto Future = Client.Get("/api/async-test/hello"); auto Resp = Future.get(); CHECK(Resp.IsSuccess()); CHECK_EQ(Resp.AsText(), "hello world"); } WorkGuard.reset(); IoThread.join(); } TEST_CASE("asynchttpclient.connection.error") { // Connect to a port where nothing is listening AsyncHttpClient Client("127.0.0.1:1", HttpClientSettings{.ConnectTimeout = std::chrono::milliseconds(500)}); auto Future = Client.Get("/should-fail"); auto Resp = Future.get(); CHECK_FALSE(Resp.IsSuccess()); CHECK(Resp.Error.has_value()); CHECK(Resp.Error->IsConnectionError()); } TEST_SUITE_END(); void asynchttpclient_test_forcelink() { } } // namespace zen #endif