#include "util/AsioContextTestFixture.hpp" #include "util/AssignRandomPort.hpp" #include "util/TestHttpServer.hpp" #include "util/requests/RequestBuilder.hpp" #include "util/requests/Types.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace util::requests; using namespace boost; using namespace boost::beast; struct RequestBuilderTestBundle { std::string testName; http::verb method; std::vector headers; std::string target; }; struct RequestBuilderTestBase : SyncAsioContextTest { TestHttpServer server{ctx_, "0.0.0.0"}; RequestBuilder builder{"localhost", server.port()}; }; struct RequestBuilderTest : RequestBuilderTestBase, testing::WithParamInterface {}; INSTANTIATE_TEST_CASE_P( RequestBuilderTest, RequestBuilderTest, testing::Values( RequestBuilderTestBundle{"GetSimple", http::verb::get, {}, "/"}, RequestBuilderTestBundle{ "GetWithHeaders", http::verb::get, {{http::field::accept, "text/html"}, {http::field::authorization, "password"}, {"Custom_header", "some_value"}}, "/" }, RequestBuilderTestBundle{"GetWithTarget", http::verb::get, {}, "/test"}, RequestBuilderTestBundle{"PostSimple", http::verb::post, {}, "/"}, RequestBuilderTestBundle{ "PostWithHeaders", http::verb::post, {{http::field::accept, "text/html"}, {http::field::authorization, "password"}, {"Custom_header", "some_value"}}, "/" }, RequestBuilderTestBundle{"PostWithTarget", http::verb::post, {}, "/test"} ), [](auto const& info) { return info.param.testName; } ); TEST_P(RequestBuilderTest, SimpleRequest) { std::string const replyBody = "Hello, world!"; builder.addHeaders(GetParam().headers); builder.setTarget(GetParam().target); server.handleRequest( [&replyBody]( http::request request ) -> std::optional> { [&]() { EXPECT_TRUE(request.target() == GetParam().target); EXPECT_TRUE(request.method() == GetParam().method); for (auto const& header : GetParam().headers) { std::visit( [&](auto const& name) { auto it = request.find(name); ASSERT_NE(it, request.end()); EXPECT_EQ(it->value(), header.value); }, header.name ); } }(); return http::response{http::status::ok, 11, replyBody}; } ); runSpawn([this, replyBody](asio::yield_context yield) { auto const response = [&]() -> std::expected { switch (GetParam().method) { case http::verb::get: return builder.getPlain(yield); case http::verb::post: return builder.postPlain(yield); default: return std::unexpected{RequestError{"Invalid HTTP verb"}}; } }(); ASSERT_TRUE(response) << response.error().message(); EXPECT_EQ(response.value(), replyBody); }); } TEST_F(RequestBuilderTest, Timeout) { builder.setTimeout(std::chrono::milliseconds{10}); server.handleRequest( []( http::request request ) -> std::optional> { [&]() { ASSERT_TRUE(request.target() == "/"); ASSERT_TRUE(request.method() == http::verb::get); }(); std::this_thread::sleep_for(std::chrono::milliseconds{20}); return std::nullopt; } ); runSpawn([this](asio::yield_context yield) { auto response = builder.getPlain(yield); EXPECT_FALSE(response); }); } TEST_F(RequestBuilderTest, RequestWithBody) { std::string const requestBody = "Hello, world!"; std::string const replyBody = "Hello, client!"; builder.addData(requestBody); server.handleRequest( [&]( http::request request ) -> std::optional> { [&]() { EXPECT_EQ(request.target(), "/"); EXPECT_EQ(request.method(), http::verb::get); EXPECT_EQ(request.body(), requestBody); }(); return http::response{http::status::ok, 11, replyBody}; } ); runSpawn([&](asio::yield_context yield) { auto const response = builder.getPlain(yield); ASSERT_TRUE(response) << response.error().message(); EXPECT_EQ(response.value(), replyBody) << response.value(); }); } TEST_F(RequestBuilderTest, ResolveError) { builder = RequestBuilder{"wrong_host", "11111"}; runSpawn([this](asio::yield_context yield) { auto const response = builder.getPlain(yield); ASSERT_FALSE(response); EXPECT_TRUE(response.error().message().starts_with("Resolve error")) << response.error().message(); }); } TEST_F(RequestBuilderTest, ConnectionError) { builder = RequestBuilder{"localhost", std::to_string(tests::util::generateFreePort())}; builder.setTimeout(std::chrono::milliseconds{1}); runSpawn([this](asio::yield_context yield) { auto const response = builder.getPlain(yield); ASSERT_FALSE(response); EXPECT_TRUE(response.error().message().starts_with("Connection error")) << response.error().message(); }); } TEST_F(RequestBuilderTest, ResponseStatusIsNotOk) { server.handleRequest([](auto&&) -> std::optional> { return http::response{http::status::not_found, 11, "Not found"}; }); runSpawn([this](asio::yield_context yield) { auto const response = builder.getPlain(yield); ASSERT_FALSE(response); EXPECT_TRUE(response.error().message().starts_with("Response status is not OK")) << response.error().message(); }); } struct RequestBuilderSslTestBundle { std::string testName; boost::beast::http::verb method; }; struct RequestBuilderSslTest : RequestBuilderTestBase, testing::WithParamInterface {}; INSTANTIATE_TEST_CASE_P( RequestBuilderSslTest, RequestBuilderSslTest, testing::Values( RequestBuilderSslTestBundle{"Get", http::verb::get}, RequestBuilderSslTestBundle{"Post", http::verb::post} ), [](auto const& info) { return info.param.testName; } ); TEST_P(RequestBuilderSslTest, TrySslUsePlain) { // First try will be SSL, but the server can't handle SSL requests server.handleRequest( [](auto&&) -> std::optional> { []() { FAIL(); }(); return std::nullopt; }, true ); server.handleRequest( [&]( http::request request ) -> std::optional> { [&]() { EXPECT_EQ(request.target(), "/"); EXPECT_EQ(request.method(), GetParam().method); }(); return http::response{http::status::ok, 11, "Hello, world!"}; } ); runSpawn([this](asio::yield_context yield) { auto const response = [&]() -> std::expected { switch (GetParam().method) { case http::verb::get: return builder.get(yield); case http::verb::post: return builder.post(yield); default: return std::unexpected{RequestError{"Invalid HTTP verb"}}; } }(); ASSERT_TRUE(response) << response.error().message(); EXPECT_EQ(response.value(), "Hello, world!"); }); }