feat: Use new web server by default (#2182)

Fixes #1781.
This commit is contained in:
Sergey Kuznetsov
2025-06-04 15:01:30 +01:00
committed by GitHub
parent 357b96ab0d
commit b3f3259b14
66 changed files with 2177 additions and 6821 deletions

View File

@@ -182,17 +182,16 @@ target_sources(
web/dosguard/IntervalSweepHandlerTests.cpp
web/dosguard/WeightsTests.cpp
web/dosguard/WhitelistHandlerTests.cpp
web/ResponseTests.cpp
web/RequestTests.cpp
web/RPCServerHandlerTests.cpp
web/ServerTests.cpp
web/SubscriptionContextTests.cpp
web/impl/ConnectionHandlerTests.cpp
web/impl/ErrorHandlingTests.cpp
web/ng/ResponseTests.cpp
web/ng/RequestTests.cpp
web/ng/RPCServerHandlerTests.cpp
web/ng/ServerTests.cpp
web/ng/SubscriptionContextTests.cpp
web/ng/impl/ConnectionHandlerTests.cpp
web/ng/impl/ErrorHandlingTests.cpp
web/ng/impl/HttpConnectionTests.cpp
web/ng/impl/ServerSslContextTests.cpp
web/ng/impl/WsConnectionTests.cpp
web/impl/HttpConnectionTests.cpp
web/impl/ServerSslContextTests.cpp
web/impl/WsConnectionTests.cpp
web/RPCServerHandlerTests.cpp
web/ServerTests.cpp
web/SubscriptionContextTests.cpp

View File

@@ -50,7 +50,6 @@ TEST_F(CliArgsTests, Parse_NoArgs)
int const returnCode = 123;
EXPECT_CALL(onRunMock, Call).WillOnce([](CliArgs::Action::Run const& run) {
EXPECT_EQ(run.configPath, CliArgs::kDEFAULT_CONFIG_PATH);
EXPECT_FALSE(run.useNgWebServer);
return returnCode;
});
EXPECT_EQ(
@@ -64,29 +63,6 @@ TEST_F(CliArgsTests, Parse_NoArgs)
);
}
TEST_F(CliArgsTests, Parse_NgWebServer)
{
for (auto& argv : {std::array{"clio_server", "-w"}, std::array{"clio_server", "--ng-web-server"}}) {
auto const action = CliArgs::parse(argv.size(), const_cast<char const**>(argv.data()));
int const returnCode = 123;
EXPECT_CALL(onRunMock, Call).WillOnce([](CliArgs::Action::Run const& run) {
EXPECT_EQ(run.configPath, CliArgs::kDEFAULT_CONFIG_PATH);
EXPECT_TRUE(run.useNgWebServer);
return returnCode;
});
EXPECT_EQ(
action.apply(
onRunMock.AsStdFunction(),
onExitMock.AsStdFunction(),
onMigrateMock.AsStdFunction(),
onVerifyMock.AsStdFunction()
),
returnCode
);
}
}
TEST_F(CliArgsTests, Parse_VersionHelp)
{
for (auto& argv :

View File

@@ -25,7 +25,7 @@
#include "util/MockPrometheus.hpp"
#include "util/MockSubscriptionManager.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "web/ng/Server.hpp"
#include "web/Server.hpp"
#include <boost/asio/executor_work_guard.hpp>
#include <boost/asio/io_context.hpp>
@@ -62,7 +62,7 @@ TEST_F(StopperTest, stopCalledMultipleTimes)
}
struct StopperMakeCallbackTest : util::prometheus::WithPrometheus, SyncAsioContextTest {
struct ServerMock : web::ng::ServerTag {
struct ServerMock : web::ServerTag {
MOCK_METHOD(void, stop, (boost::asio::yield_context), ());
};

View File

@@ -27,12 +27,12 @@
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/AdminVerificationStrategy.hpp"
#include "web/Connection.hpp"
#include "web/MockConnection.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/dosguard/DOSGuardMock.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/MockConnection.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/beast/core/flat_buffer.hpp>
@@ -108,7 +108,7 @@ struct MetricsHandlerTests : util::prometheus::WithPrometheus, SyncAsioContextTe
};
MetricsHandler metricsHandler{adminVerifier};
web::ng::Request request{http::request<http::string_body>{http::verb::get, "/metrics", 11}};
web::Request request{http::request<http::string_body>{http::verb::get, "/metrics", 11}};
};
TEST_F(MetricsHandlerTests, Call)
@@ -122,7 +122,7 @@ TEST_F(MetricsHandlerTests, Call)
}
struct HealthCheckHandlerTests : SyncAsioContextTest, WebHandlersTest {
web::ng::Request request{http::request<http::string_body>{http::verb::get, "/", 11}};
web::Request request{http::request<http::string_body>{http::verb::get, "/", 11}};
HealthCheckHandler healthCheckHandler;
};
@@ -142,19 +142,19 @@ struct RequestHandlerTest : SyncAsioContextTest, WebHandlersTest {
struct RpcHandlerMock {
MOCK_METHOD(
web::ng::Response,
web::Response,
call,
(web::ng::Request const&,
web::ng::ConnectionMetadata const&,
(web::Request const&,
web::ConnectionMetadata const&,
web::SubscriptionContextPtr,
boost::asio::yield_context),
()
);
web::ng::Response
web::Response
operator()(
web::ng::Request const& request,
web::ng::ConnectionMetadata const& connectionMetadata,
web::Request const& request,
web::ConnectionMetadata const& connectionMetadata,
web::SubscriptionContextPtr subscriptionContext,
boost::asio::yield_context yield
)
@@ -170,7 +170,7 @@ struct RequestHandlerTest : SyncAsioContextTest, WebHandlersTest {
TEST_F(RequestHandlerTest, RpcHandlerThrows)
{
web::ng::Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
web::Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
EXPECT_CALL(*adminVerifier, isAdmin).WillOnce(testing::Return(true));
EXPECT_CALL(rpcHandler, call).WillOnce(testing::Throw(std::runtime_error{"some error"}));
@@ -191,9 +191,9 @@ TEST_F(RequestHandlerTest, RpcHandlerThrows)
TEST_F(RequestHandlerTest, NoErrors)
{
web::ng::Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
web::ng::Response const response{http::status::ok, "some response", request};
auto const httpResponse = web::ng::Response{response}.intoHttpResponse();
web::Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
web::Response const response{http::status::ok, "some response", request};
auto const httpResponse = web::Response{response}.intoHttpResponse();
EXPECT_CALL(*adminVerifier, isAdmin).WillOnce(testing::Return(true));
EXPECT_CALL(rpcHandler, call).WillOnce(testing::Return(response));

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@
//==============================================================================
#include "util/NameGenerator.hpp"
#include "web/ng/Request.hpp"
#include "web/Request.hpp"
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
@@ -31,7 +31,7 @@
#include <string>
#include <utility>
using namespace web::ng;
using namespace web;
namespace http = boost::beast::http;
struct RequestTest : public ::testing::Test {

View File

@@ -23,9 +23,9 @@
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/ng/MockConnection.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/MockConnection.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http/field.hpp>
@@ -42,7 +42,7 @@
#include <string>
#include <utility>
using namespace web::ng;
using namespace web;
namespace http = boost::beast::http;
using namespace util::config;

File diff suppressed because it is too large Load Diff

View File

@@ -17,62 +17,156 @@
*/
//==============================================================================
#include "util/LoggerFixtures.hpp"
#include "util/AsioContextTestFixture.hpp"
#include "util/Taggable.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/Connection.hpp"
#include "web/Error.hpp"
#include "web/SubscriptionContext.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/interface/ConnectionBaseMock.hpp"
#include "web/impl/MockWsConnection.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/system/errc.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <cstddef>
#include <memory>
#include <optional>
#include <string>
using namespace web;
using namespace util::config;
struct SubscriptionContextTests : NoLoggerFixture {
struct SubscriptionContextTests : SyncAsioContextTest {
SubscriptionContext
makeSubscriptionContext(boost::asio::yield_context yield, std::optional<size_t> maxSendQueueSize = std::nullopt)
{
return SubscriptionContext{tagFactory_, connection_, maxSendQueueSize, yield, errorHandler_.AsStdFunction()};
}
protected:
util::TagDecoratorFactory tagFactory_{ClioConfigDefinition{
{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
}};
ConnectionBaseStrictMockPtr connection_ =
std::make_shared<testing::StrictMock<ConnectionBaseMock>>(tagFactory_, "some ip");
SubscriptionContext subscriptionContext_{tagFactory_, connection_};
testing::StrictMock<testing::MockFunction<void(SubscriptionContextInterface*)>> callbackMock_;
MockWsConnectionImpl connection_{"some ip", boost::beast::flat_buffer{}, tagFactory_};
testing::StrictMock<testing::MockFunction<bool(web::Error const&, Connection const&)>> errorHandler_;
};
TEST_F(SubscriptionContextTests, send)
TEST_F(SubscriptionContextTests, Send)
{
auto message = std::make_shared<std::string>("message");
EXPECT_CALL(*connection_, send(message));
subscriptionContext_.send(message);
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message = std::make_shared<std::string>("some message");
EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto&&) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
return std::nullopt;
});
subscriptionContext.send(message);
subscriptionContext.disconnect(yield);
});
}
TEST_F(SubscriptionContextTests, sendConnectionExpired)
TEST_F(SubscriptionContextTests, SendOrder)
{
auto message = std::make_shared<std::string>("message");
connection_.reset();
subscriptionContext_.send(message);
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message1 = std::make_shared<std::string>("message1");
auto const message2 = std::make_shared<std::string>("message2");
testing::Sequence const sequence;
EXPECT_CALL(connection_, sendBuffer)
.InSequence(sequence)
.WillOnce([&message1](boost::asio::const_buffer buffer, auto&&) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message1);
return std::nullopt;
});
EXPECT_CALL(connection_, sendBuffer)
.InSequence(sequence)
.WillOnce([&message2](boost::asio::const_buffer buffer, auto&&) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message2);
return std::nullopt;
});
subscriptionContext.send(message1);
subscriptionContext.send(message2);
subscriptionContext.disconnect(yield);
});
}
TEST_F(SubscriptionContextTests, onDisconnect)
TEST_F(SubscriptionContextTests, SendFailed)
{
auto localContext = std::make_unique<SubscriptionContext>(tagFactory_, connection_);
localContext->onDisconnect(callbackMock_.AsStdFunction());
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message = std::make_shared<std::string>("some message");
EXPECT_CALL(callbackMock_, Call(localContext.get()));
localContext.reset();
EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto&&) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
return boost::system::errc::make_error_code(boost::system::errc::not_supported);
});
EXPECT_CALL(errorHandler_, Call).WillOnce(testing::Return(true));
EXPECT_CALL(connection_, close);
subscriptionContext.send(message);
subscriptionContext.disconnect(yield);
});
}
TEST_F(SubscriptionContextTests, setApiSubversion)
TEST_F(SubscriptionContextTests, SendTooManySubscriptions)
{
EXPECT_EQ(subscriptionContext_.apiSubversion(), 0);
subscriptionContext_.setApiSubversion(42);
EXPECT_EQ(subscriptionContext_.apiSubversion(), 42);
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield, 1);
auto const message = std::make_shared<std::string>("message1");
EXPECT_CALL(connection_, sendBuffer)
.WillOnce([&message](boost::asio::const_buffer buffer, boost::asio::yield_context innerYield) {
boost::asio::post(innerYield); // simulate send is slow by switching to another coroutine
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
return std::nullopt;
});
EXPECT_CALL(connection_, close);
subscriptionContext.send(message);
subscriptionContext.send(message);
subscriptionContext.send(message);
subscriptionContext.disconnect(yield);
});
}
TEST_F(SubscriptionContextTests, SendAfterDisconnect)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message = std::make_shared<std::string>("some message");
subscriptionContext.disconnect(yield);
subscriptionContext.send(message);
});
}
TEST_F(SubscriptionContextTests, OnDisconnect)
{
testing::StrictMock<testing::MockFunction<void(web::SubscriptionContextInterface*)>> onDisconnect;
runSpawn([&](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
subscriptionContext.onDisconnect(onDisconnect.AsStdFunction());
EXPECT_CALL(onDisconnect, Call(&subscriptionContext));
subscriptionContext.disconnect(yield);
});
}
TEST_F(SubscriptionContextTests, SetApiSubversion)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
subscriptionContext.setApiSubversion(42);
EXPECT_EQ(subscriptionContext.apiSubversion(), 42);
});
}

View File

@@ -24,15 +24,15 @@
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/Connection.hpp"
#include "web/Error.hpp"
#include "web/ProcessingPolicy.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/ProcessingPolicy.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/ConnectionHandler.hpp"
#include "web/ng/impl/MockHttpConnection.hpp"
#include "web/ng/impl/MockWsConnection.hpp"
#include "web/impl/ConnectionHandler.hpp"
#include "web/impl/MockHttpConnection.hpp"
#include "web/impl/MockWsConnection.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/error.hpp>
@@ -57,8 +57,8 @@
#include <string>
#include <utility>
using namespace web::ng::impl;
using namespace web::ng;
using namespace web::impl;
using namespace web;
using namespace util;
using testing::Return;
namespace beast = boost::beast;

View File

@@ -20,101 +20,213 @@
#include "rpc/Errors.hpp"
#include "util/LoggerFixtures.hpp"
#include "util/NameGenerator.hpp"
#include "util/Taggable.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/Request.hpp"
#include "web/impl/ErrorHandling.hpp"
#include "web/interface/ConnectionBaseMock.hpp"
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <variant>
using namespace web::impl;
using namespace web;
using namespace util::config;
namespace http = boost::beast::http;
struct ErrorHandlingTests : NoLoggerFixture {
protected:
util::TagDecoratorFactory tagFactory_{ClioConfigDefinition{
{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
}};
std::string const clientIp_ = "some ip";
ConnectionBaseStrictMockPtr connection_ =
std::make_shared<testing::StrictMock<ConnectionBaseMock>>(tagFactory_, clientIp_);
static Request
makeRequest(bool isHttp, std::optional<std::string> body = std::nullopt)
{
if (isHttp)
return Request{http::request<http::string_body>{http::verb::post, "/", 11, body.value_or("")}};
static Request::HttpHeaders const kHEADERS;
return Request{body.value_or(""), kHEADERS};
}
};
struct ErrorHandlingComposeErrorTestBundle {
struct ErrorHandlingMakeErrorTestBundle {
std::string testName;
bool connectionUpgraded;
std::optional<boost::json::object> request;
bool isHttp;
rpc::Status status;
std::string expectedMessage;
boost::beast::http::status expectedStatus;
};
struct ErrorHandlingMakeErrorTest : ErrorHandlingTests,
testing::WithParamInterface<ErrorHandlingMakeErrorTestBundle> {};
TEST_P(ErrorHandlingMakeErrorTest, MakeError)
{
auto const request = makeRequest(GetParam().isHttp);
ErrorHelper const errorHelper{request};
auto response = errorHelper.makeError(GetParam().status);
EXPECT_EQ(response.message(), GetParam().expectedMessage);
if (GetParam().isHttp) {
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), GetParam().expectedStatus);
std::string expectedContentType = "text/html";
if (std::holds_alternative<rpc::RippledError>(GetParam().status.code))
expectedContentType = "application/json";
EXPECT_EQ(httpResponse.at(http::field::content_type), expectedContentType);
}
}
INSTANTIATE_TEST_CASE_P(
ErrorHandlingMakeErrorTestGroup,
ErrorHandlingMakeErrorTest,
testing::ValuesIn({
ErrorHandlingMakeErrorTestBundle{
"WsRequest",
false,
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON",
boost::beast::http::status::ok
},
ErrorHandlingMakeErrorTestBundle{
"HttpRequest_InvalidApiVersion",
true,
rpc::Status{rpc::ClioError::RpcInvalidApiVersion},
"invalid_API_version",
boost::beast::http::status::bad_request
},
ErrorHandlingMakeErrorTestBundle{
"HttpRequest_CommandIsMissing",
true,
rpc::Status{rpc::ClioError::RpcCommandIsMissing},
"Null method",
boost::beast::http::status::bad_request
},
ErrorHandlingMakeErrorTestBundle{
"HttpRequest_CommandIsEmpty",
true,
rpc::Status{rpc::ClioError::RpcCommandIsEmpty},
"method is empty",
boost::beast::http::status::bad_request
},
ErrorHandlingMakeErrorTestBundle{
"HttpRequest_CommandNotString",
true,
rpc::Status{rpc::ClioError::RpcCommandNotString},
"method is not string",
boost::beast::http::status::bad_request
},
ErrorHandlingMakeErrorTestBundle{
"HttpRequest_ParamsUnparsable",
true,
rpc::Status{rpc::ClioError::RpcParamsUnparsable},
"params unparsable",
boost::beast::http::status::bad_request
},
ErrorHandlingMakeErrorTestBundle{
"HttpRequest_RippledError",
true,
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
R"JSON({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})JSON",
boost::beast::http::status::bad_request
},
}),
tests::util::kNAME_GENERATOR
);
struct ErrorHandlingMakeInternalErrorTestBundle {
std::string testName;
bool isHttp;
std::optional<std::string> request;
boost::json::object expectedResult;
};
struct ErrorHandlingComposeErrorTest : ErrorHandlingTests,
testing::WithParamInterface<ErrorHandlingComposeErrorTestBundle> {};
struct ErrorHandlingMakeInternalErrorTest : ErrorHandlingTests,
testing::WithParamInterface<ErrorHandlingMakeInternalErrorTestBundle> {};
TEST_P(ErrorHandlingComposeErrorTest, composeError)
TEST_P(ErrorHandlingMakeInternalErrorTest, ComposeError)
{
connection_->upgraded = GetParam().connectionUpgraded;
ErrorHelper const errorHelper{connection_, GetParam().request};
auto const result = errorHelper.composeError(rpc::RippledError::rpcNOT_READY);
EXPECT_EQ(boost::json::serialize(result), boost::json::serialize(GetParam().expectedResult));
auto const request = makeRequest(GetParam().isHttp, GetParam().request);
std::optional<boost::json::object> const requestJson = GetParam().request.has_value()
? std::make_optional(boost::json::parse(*GetParam().request).as_object())
: std::nullopt;
ErrorHelper const errorHelper{request, requestJson};
auto response = errorHelper.makeInternalError();
EXPECT_EQ(response.message(), boost::json::serialize(GetParam().expectedResult));
if (GetParam().isHttp) {
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::internal_server_error);
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
}
}
INSTANTIATE_TEST_CASE_P(
ErrorHandlingComposeErrorTestGroup,
ErrorHandlingComposeErrorTest,
ErrorHandlingMakeInternalErrorTest,
testing::ValuesIn(
{ErrorHandlingComposeErrorTestBundle{
"NoRequest_UpgradedConnection",
true,
{ErrorHandlingMakeInternalErrorTestBundle{
"NoRequest_WebsocketConnection",
false,
std::nullopt,
{{"error", "notReady"},
{"error_code", 13},
{"error_message", "Not ready to handle this request."},
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"}}
},
ErrorHandlingComposeErrorTestBundle{
"NoRequest_NotUpgradedConnection",
false,
ErrorHandlingMakeInternalErrorTestBundle{
"NoRequest_HttpConnection",
true,
std::nullopt,
{{"result",
{{"error", "notReady"},
{"error_code", 13},
{"error_message", "Not ready to handle this request."},
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"}}}}
},
ErrorHandlingComposeErrorTestBundle{
"Request_UpgradedConnection",
true,
boost::json::object{{"id", 1}, {"api_version", 2}},
{{"error", "notReady"},
{"error_code", 13},
{"error_message", "Not ready to handle this request."},
ErrorHandlingMakeInternalErrorTestBundle{
"Request_WebsocketConnection",
false,
std::string{R"JSON({"id": 1, "api_version": 2})JSON"},
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"},
{"id", 1},
{"api_version", 2},
{"request", {{"id", 1}, {"api_version", 2}}}}
},
ErrorHandlingComposeErrorTestBundle{
"Request_NotUpgradedConnection",
ErrorHandlingMakeInternalErrorTestBundle{
"Request_WebsocketConnection_NoId",
false,
boost::json::object{{"id", 1}, {"api_version", 2}},
std::string{R"JSON({"api_version": 2})JSON"},
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"},
{"api_version", 2},
{"request", {{"api_version", 2}}}}
},
ErrorHandlingMakeInternalErrorTestBundle{
"Request_HttpConnection",
true,
std::string{R"JSON({"id": 1, "api_version": 2})JSON"},
{{"result",
{{"error", "notReady"},
{"error_code", 13},
{"error_message", "Not ready to handle this request."},
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"},
{"id", 1},
@@ -124,169 +236,122 @@ INSTANTIATE_TEST_CASE_P(
tests::util::kNAME_GENERATOR
);
struct ErrorHandlingSendErrorTestBundle {
TEST_F(ErrorHandlingTests, MakeNotReadyError)
{
auto const request = makeRequest(true);
auto response = ErrorHelper{request}.makeNotReadyError();
EXPECT_EQ(
response.message(),
std::string{
R"JSON({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})JSON"
}
);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
}
TEST_F(ErrorHandlingTests, MakeTooBusyError_WebsocketRequest)
{
auto const request = makeRequest(false);
auto response = ErrorHelper{request}.makeTooBusyError();
EXPECT_EQ(
response.message(),
std::string{
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
}
);
}
TEST_F(ErrorHandlingTests, sendTooBusyError_HttpConnection)
{
auto const request = makeRequest(true);
auto response = ErrorHelper{request}.makeTooBusyError();
EXPECT_EQ(
response.message(),
std::string{
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
}
);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::service_unavailable);
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
}
TEST_F(ErrorHandlingTests, makeJsonParsingError_WebsocketConnection)
{
auto const request = makeRequest(false);
auto response = ErrorHelper{request}.makeJsonParsingError();
EXPECT_EQ(
response.message(),
std::string{
R"JSON({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})JSON"
}
);
}
TEST_F(ErrorHandlingTests, makeJsonParsingError_HttpConnection)
{
auto const request = makeRequest(true);
auto response = ErrorHelper{request}.makeJsonParsingError();
EXPECT_EQ(response.message(), std::string{"Unable to parse JSON from the request"});
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::bad_request);
EXPECT_EQ(httpResponse.at(http::field::content_type), "text/html");
}
struct ErrorHandlingComposeErrorTestBundle {
std::string testName;
bool connectionUpgraded;
rpc::Status status;
bool isHttp;
std::optional<boost::json::object> request;
std::string expectedMessage;
boost::beast::http::status expectedStatus;
};
struct ErrorHandlingSendErrorTest : ErrorHandlingTests,
testing::WithParamInterface<ErrorHandlingSendErrorTestBundle> {};
struct ErrorHandlingComposeErrorTest : ErrorHandlingTests,
testing::WithParamInterface<ErrorHandlingComposeErrorTestBundle> {};
TEST_P(ErrorHandlingSendErrorTest, sendError)
TEST_P(ErrorHandlingComposeErrorTest, ComposeError)
{
connection_->upgraded = GetParam().connectionUpgraded;
ErrorHelper const errorHelper{connection_};
EXPECT_CALL(*connection_, send(std::string{GetParam().expectedMessage}, GetParam().expectedStatus));
errorHelper.sendError(GetParam().status);
auto const request = makeRequest(GetParam().isHttp);
ErrorHelper const errorHelper{request, GetParam().request};
auto const response = errorHelper.composeError(rpc::Status{rpc::RippledError::rpcINTERNAL});
EXPECT_EQ(boost::json::serialize(response), GetParam().expectedMessage);
}
INSTANTIATE_TEST_CASE_P(
ErrorHandlingSendErrorTestGroup,
ErrorHandlingSendErrorTest,
testing::ValuesIn({
ErrorHandlingSendErrorTestBundle{
"UpgradedConnection",
true,
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON",
boost::beast::http::status::ok
},
ErrorHandlingSendErrorTestBundle{
"NotUpgradedConnection_InvalidApiVersion",
false,
rpc::Status{rpc::ClioError::RpcInvalidApiVersion},
"invalid_API_version",
boost::beast::http::status::bad_request
},
ErrorHandlingSendErrorTestBundle{
"NotUpgradedConnection_CommandIsMissing",
false,
rpc::Status{rpc::ClioError::RpcCommandIsMissing},
"Null method",
boost::beast::http::status::bad_request
},
ErrorHandlingSendErrorTestBundle{
"NotUpgradedConnection_CommandIsEmpty",
false,
rpc::Status{rpc::ClioError::RpcCommandIsEmpty},
"method is empty",
boost::beast::http::status::bad_request
},
ErrorHandlingSendErrorTestBundle{
"NotUpgradedConnection_CommandNotString",
false,
rpc::Status{rpc::ClioError::RpcCommandNotString},
"method is not string",
boost::beast::http::status::bad_request
},
ErrorHandlingSendErrorTestBundle{
"NotUpgradedConnection_ParamsUnparsable",
false,
rpc::Status{rpc::ClioError::RpcParamsUnparsable},
"params unparsable",
boost::beast::http::status::bad_request
},
ErrorHandlingSendErrorTestBundle{
"NotUpgradedConnection_RippledError",
false,
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
R"JSON({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})JSON",
boost::beast::http::status::bad_request
},
}),
ErrorHandlingComposeErrorTestGroup,
ErrorHandlingComposeErrorTest,
testing::ValuesIn(
{ErrorHandlingComposeErrorTestBundle{
"NoRequest_WebsocketConnection",
false,
std::nullopt,
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})JSON"
},
ErrorHandlingComposeErrorTestBundle{
"NoRequest_HttpConnection",
true,
std::nullopt,
R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"}})JSON"
},
ErrorHandlingComposeErrorTestBundle{
"Request_WebsocketConnection",
false,
boost::json::object{{"id", 1}, {"api_version", 2}},
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"api_version":2,"request":{"id":1,"api_version":2}})JSON",
},
ErrorHandlingComposeErrorTestBundle{
"Request_WebsocketConnection_NoId",
false,
boost::json::object{{"api_version", 2}},
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","api_version":2,"request":{"api_version":2}})JSON",
},
ErrorHandlingComposeErrorTestBundle{
"Request_HttpConnection",
true,
boost::json::object{{"id", 1}, {"api_version", 2}},
R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"request":{"id":1,"api_version":2}}})JSON"
}}
),
tests::util::kNAME_GENERATOR
);
TEST_F(ErrorHandlingTests, sendInternalError)
{
ErrorHelper const errorHelper{connection_};
EXPECT_CALL(
*connection_,
send(
std::string{
R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"}})JSON"
},
boost::beast::http::status::internal_server_error
)
);
errorHelper.sendInternalError();
}
TEST_F(ErrorHandlingTests, sendNotReadyError)
{
ErrorHelper const errorHelper{connection_};
EXPECT_CALL(
*connection_,
send(
std::string{
R"JSON({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})JSON"
},
boost::beast::http::status::ok
)
);
errorHelper.sendNotReadyError();
}
TEST_F(ErrorHandlingTests, sendTooBusyError_UpgradedConnection)
{
connection_->upgraded = true;
ErrorHelper const errorHelper{connection_};
EXPECT_CALL(
*connection_,
send(
std::string{
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
},
boost::beast::http::status::ok
)
);
errorHelper.sendTooBusyError();
}
TEST_F(ErrorHandlingTests, sendTooBusyError_NotUpgradedConnection)
{
connection_->upgraded = false;
ErrorHelper const errorHelper{connection_};
EXPECT_CALL(
*connection_,
send(
std::string{
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
},
boost::beast::http::status::service_unavailable
)
);
errorHelper.sendTooBusyError();
}
TEST_F(ErrorHandlingTests, sendJsonParsingError_UpgradedConnection)
{
connection_->upgraded = true;
ErrorHelper const errorHelper{connection_};
EXPECT_CALL(
*connection_,
send(
std::string{
R"JSON({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})JSON"
},
boost::beast::http::status::ok
)
);
errorHelper.sendJsonParsingError();
}
TEST_F(ErrorHandlingTests, sendJsonParsingError_NotUpgradedConnection)
{
connection_->upgraded = false;
ErrorHelper const errorHelper{connection_};
EXPECT_CALL(
*connection_,
send(std::string{"Unable to parse JSON from the request"}, boost::beast::http::status::bad_request)
);
errorHelper.sendJsonParsingError();
}

View File

@@ -25,9 +25,9 @@
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/HttpConnection.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include "web/impl/HttpConnection.hpp"
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
@@ -46,8 +46,8 @@
#include <ranges>
#include <utility>
using namespace web::ng::impl;
using namespace web::ng;
using namespace web::impl;
using namespace web;
using namespace util::config;
namespace http = boost::beast::http;

View File

@@ -22,7 +22,7 @@
#include "util/config/ConfigFileJson.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/ng/impl/ServerSslContext.hpp"
#include "web/impl/ServerSslContext.hpp"
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
@@ -33,7 +33,7 @@
#include <optional>
#include <string>
using namespace web::ng::impl;
using namespace web::impl;
using namespace util::config;
struct MakeServerSslContextFromConfigTestBundle {

View File

@@ -25,11 +25,11 @@
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/HttpConnection.hpp"
#include "web/ng/impl/WsConnection.hpp"
#include "web/Error.hpp"
#include "web/Request.hpp"
#include "web/Response.hpp"
#include "web/impl/HttpConnection.hpp"
#include "web/impl/WsConnection.hpp"
#include <boost/asio/error.hpp>
#include <boost/asio/executor_work_guard.hpp>
@@ -51,8 +51,8 @@
#include <thread>
#include <utility>
using namespace web::ng::impl;
using namespace web::ng;
using namespace web::impl;
using namespace web;
using namespace util;
struct WebWsConnectionTests : SyncAsioContextTest {

View File

@@ -1,611 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "rpc/Errors.hpp"
#include "rpc/common/Types.hpp"
#include "util/AsioContextTestFixture.hpp"
#include "util/MockBackendTestFixture.hpp"
#include "util/MockETLService.hpp"
#include "util/MockPrometheus.hpp"
#include "util/MockRPCEngine.hpp"
#include "util/Taggable.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/dosguard/DOSGuardMock.hpp"
#include "web/ng/MockConnection.hpp"
#include "web/ng/RPCServerHandler.hpp"
#include "web/ng/Request.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <cstdint>
#include <iterator>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_set>
#include <utility>
using namespace web::ng;
using testing::Return;
using testing::StrictMock;
using namespace util::config;
namespace http = boost::beast::http;
struct NgRpcServerHandlerTest : util::prometheus::WithPrometheus, MockBackendTestStrict, SyncAsioContextTest {
ClioConfigDefinition config{ClioConfigDefinition{
{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
{"api_version.min", ConfigValue{ConfigType::Integer}.defaultValue(1)},
{"api_version.max", ConfigValue{ConfigType::Integer}.defaultValue(2)},
{"api_version.default", ConfigValue{ConfigType::Integer}.defaultValue(1)}
}};
protected:
std::shared_ptr<testing::StrictMock<MockRPCEngine>> rpcEngine_ =
std::make_shared<testing::StrictMock<MockRPCEngine>>();
std::shared_ptr<StrictMock<MockETLService>> etl_ = std::make_shared<StrictMock<MockETLService>>();
DOSGuardStrictMock dosguard_;
RPCServerHandler<MockRPCEngine> rpcServerHandler_{config, backend_, rpcEngine_, etl_, dosguard_};
util::TagDecoratorFactory tagFactory_{config};
std::string const ip_ = "some ip";
StrictMockConnectionMetadata connectionMetadata_{ip_, tagFactory_};
Request::HttpHeaders const httpHeaders_;
static Request
makeHttpRequest(std::string_view body)
{
return Request{http::request<http::string_body>{http::verb::post, "/", 11, body}};
}
Request
makeWsRequest(std::string body)
{
return Request{std::move(body), httpHeaders_};
}
};
TEST_F(NgRpcServerHandlerTest, DosguardRejectedHttpRequest)
{
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest("some message");
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(false));
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const responseHttp = std::move(response).intoHttpResponse();
EXPECT_EQ(responseHttp.result(), http::status::service_unavailable);
auto const responseJson = boost::json::parse(responseHttp.body()).as_object();
EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
});
}
TEST_F(NgRpcServerHandlerTest, DosguardRejectedWsRequest)
{
runSpawn([&](boost::asio::yield_context yield) {
auto const requestStr = "some message";
auto const request = makeWsRequest(requestStr);
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(false));
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const responseWs = boost::beast::buffers_to_string(response.asWsResponse());
auto const responseJson = boost::json::parse(responseWs).as_object();
EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
EXPECT_EQ(responseJson.at("request").as_string(), requestStr);
});
}
TEST_F(NgRpcServerHandlerTest, DosguardRejectedWsJsonRequest)
{
runSpawn([&](boost::asio::yield_context yield) {
auto const requestStr = R"JSON({"request": "some message", "id": "some id"})JSON";
auto const request = makeWsRequest(requestStr);
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(false));
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const responseWs = boost::beast::buffers_to_string(response.asWsResponse());
auto const responseJson = boost::json::parse(responseWs).as_object();
EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
EXPECT_EQ(responseJson.at("request").as_string(), requestStr);
EXPECT_EQ(responseJson.at("id").as_string(), "some id");
});
}
TEST_F(NgRpcServerHandlerTest, PostToRpcEngineFailed)
{
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest("some message");
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(*rpcEngine_, post).WillOnce(Return(false));
EXPECT_CALL(*rpcEngine_, notifyTooBusy());
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::service_unavailable);
});
}
TEST_F(NgRpcServerHandlerTest, CoroutineSleepsUntilRpcEngineFinishes)
{
StrictMock<testing::MockFunction<void()>> rpcServerHandlerDone;
StrictMock<testing::MockFunction<void()>> rpcEngineDone;
testing::Expectation const expectedRpcEngineDone = EXPECT_CALL(rpcEngineDone, Call);
EXPECT_CALL(rpcServerHandlerDone, Call).After(expectedRpcEngineDone);
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest("some message");
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
boost::asio::spawn(
ctx_,
[this, &rpcEngineDone, fn = std::forward<decltype(fn)>(fn)](boost::asio::yield_context yield) {
EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
fn(yield);
rpcEngineDone.Call();
}
);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
rpcServerHandlerDone.Call();
EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request);
});
}
TEST_F(NgRpcServerHandlerTest, JsonParseFailed)
{
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest("not a json");
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request);
});
}
TEST_F(NgRpcServerHandlerTest, DosguardRejectedParsedRequest)
{
runSpawn([&](boost::asio::yield_context yield) {
std::string const requestStr = "{}";
auto const request = makeHttpRequest(requestStr);
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(false));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
fn(yield);
return true;
});
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const responseHttp = std::move(response).intoHttpResponse();
EXPECT_EQ(responseHttp.result(), http::status::service_unavailable);
auto const responseJson = boost::json::parse(responseHttp.body()).as_object();
EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
});
}
TEST_F(NgRpcServerHandlerTest, DosguardAddsLoadWarning)
{
runSpawn([&](boost::asio::yield_context yield) {
std::string const requestStr = "{}";
auto const request = makeHttpRequest(requestStr);
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(false));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
fn(yield);
return true;
});
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(false));
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const responseHttp = std::move(response).intoHttpResponse();
EXPECT_EQ(responseHttp.result(), http::status::service_unavailable);
auto const responseJson = boost::json::parse(responseHttp.body()).as_object();
EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN);
EXPECT_EQ(responseJson.at("warning").as_string(), "load");
EXPECT_EQ(responseJson.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcRateLimit);
});
}
TEST_F(NgRpcServerHandlerTest, GotNotJsonObject)
{
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest("[]");
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request);
});
}
TEST_F(NgRpcServerHandlerTest, HandleRequest_NoRangeFromBackend)
{
runSpawn([&](boost::asio::yield_context yield) {
std::string const requestStr = "{}";
auto const request = makeHttpRequest(requestStr);
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillOnce(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, notifyNotReady);
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "notReady");
});
}
TEST_F(NgRpcServerHandlerTest, HandleRequest_ContextCreationFailed)
{
backend_->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
std::string const requestStr = "{}";
auto const request = makeHttpRequest(requestStr);
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::bad_request);
EXPECT_EQ(httpResponse.body(), "Null method");
});
}
TEST_F(NgRpcServerHandlerTest, HandleRequest_BuildResponseFailed)
{
backend_->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
std::string const requestStr = R"JSON({"method":"some_method"})JSON";
auto const request = makeHttpRequest(requestStr);
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse)
.WillOnce(Return(rpc::Result{rpc::Status{rpc::ClioError::RpcUnknownOption}}));
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "unknownOption");
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1);
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
});
}
TEST_F(NgRpcServerHandlerTest, HandleRequest_BuildResponseThrewAnException)
{
backend_->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
std::string const requestStr = R"JSON({"method":"some_method"})JSON";
auto const request = makeHttpRequest(requestStr);
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse).WillOnce([](auto&&) -> rpc::Result {
throw std::runtime_error("some error");
});
EXPECT_CALL(*rpcEngine_, notifyInternalError);
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::internal_server_error);
});
}
TEST_F(NgRpcServerHandlerTest, HandleRequest_Successful_HttpRequest)
{
backend_->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
std::string const requestStr = R"JSON({"method":"some_method"})JSON";
auto const request = makeHttpRequest(requestStr);
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse)
.WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{{"some key", "some value"}}}}));
EXPECT_CALL(*rpcEngine_, notifyComplete);
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
EXPECT_EQ(jsonResponse.at("result").at("status").as_string(), "success");
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
});
}
TEST_F(NgRpcServerHandlerTest, HandleRequest_OutdatedWarning)
{
backend_->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
std::string const requestStr = R"JSON({"method":"some_method"})JSON";
auto const request = makeHttpRequest(requestStr);
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse)
.WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{{"some key", "some value"}}}}));
EXPECT_CALL(*rpcEngine_, notifyComplete);
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(61));
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
std::unordered_set<int64_t> warningCodes;
std::ranges::transform(
jsonResponse.at("warnings").as_array(),
std::inserter(warningCodes, warningCodes.end()),
[](auto const& w) { return w.as_object().at("id").as_int64(); }
);
EXPECT_EQ(warningCodes.size(), 2);
EXPECT_TRUE(warningCodes.contains(rpc::WarnRpcClio));
EXPECT_TRUE(warningCodes.contains(rpc::WarnRpcOutdated));
});
}
TEST_F(NgRpcServerHandlerTest, HandleRequest_Successful_HttpRequest_Forwarded)
{
backend_->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
std::string const requestStr = R"JSON({"method":"some_method"})JSON";
auto const request = makeHttpRequest(requestStr);
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse)
.WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{
{"result", boost::json::object{{"some key", "some value"}}}, {"forwarded", true}
}}}));
EXPECT_CALL(*rpcEngine_, notifyComplete);
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
EXPECT_EQ(jsonResponse.at("result").at("status").as_string(), "success");
EXPECT_EQ(jsonResponse.at("forwarded").as_bool(), true);
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
});
}
TEST_F(NgRpcServerHandlerTest, HandleRequest_Successful_HttpRequest_HasError)
{
backend_->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
std::string const requestStr = R"JSON({"method":"some_method"})JSON";
auto const request = makeHttpRequest(requestStr);
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse)
.WillOnce(Return(rpc::Result{
rpc::ReturnType{boost::json::object{{"some key", "some value"}, {"error", "some error"}}}
}));
EXPECT_CALL(*rpcEngine_, notifyComplete);
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "some error");
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
});
}
struct NgRpcServerHandlerWsTest : NgRpcServerHandlerTest {
struct MockSubscriptionContext : web::SubscriptionContextInterface {
using web::SubscriptionContextInterface::SubscriptionContextInterface;
MOCK_METHOD(void, send, (std::shared_ptr<std::string>), (override));
MOCK_METHOD(void, onDisconnect, (web::SubscriptionContextInterface::OnDisconnectSlot const&), (override));
MOCK_METHOD(void, setApiSubversion, (uint32_t), (override));
MOCK_METHOD(uint32_t, apiSubversion, (), (const, override));
};
using StrictMockSubscriptionContext = testing::StrictMock<MockSubscriptionContext>;
protected:
std::shared_ptr<StrictMockSubscriptionContext> subscriptionContext_ =
std::make_shared<StrictMockSubscriptionContext>(tagFactory_);
};
TEST_F(NgRpcServerHandlerWsTest, HandleRequest_Successful_WsRequest)
{
backend_->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
Request::HttpHeaders const headers;
std::string const requestStr = R"JSON({"method":"some_method", "id": 1234, "api_version": 1})JSON";
auto const request = Request(requestStr, headers);
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse)
.WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{{"some key", "some value"}}}}));
EXPECT_CALL(*rpcEngine_, notifyComplete);
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
fn(yield);
return true;
});
auto const response = rpcServerHandler_(request, connectionMetadata_, subscriptionContext_, yield);
auto const jsonResponse = boost::json::parse(response.message()).as_object();
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
EXPECT_EQ(jsonResponse.at("status").as_string(), "success");
EXPECT_EQ(jsonResponse.at("type").as_string(), "response");
EXPECT_EQ(jsonResponse.at("id").as_int64(), 1234);
EXPECT_EQ(jsonResponse.at("api_version").as_int64(), 1);
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
});
}
TEST_F(NgRpcServerHandlerWsTest, HandleRequest_Successful_WsRequest_HasError)
{
backend_->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
Request::HttpHeaders const headers;
std::string const requestStr = R"JSON({"method":"some_method", "id": 1234, "api_version": 1})JSON";
auto const request = Request(requestStr, headers);
EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true));
EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())).WillOnce(Return(true));
EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true));
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse)
.WillOnce(Return(rpc::Result{
rpc::ReturnType{boost::json::object{{"some key", "some value"}, {"error", "some error"}}}
}));
EXPECT_CALL(*rpcEngine_, notifyComplete);
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
fn(yield);
return true;
});
auto const response = rpcServerHandler_(request, connectionMetadata_, subscriptionContext_, yield);
auto const jsonResponse = boost::json::parse(response.message()).as_object();
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "some error");
EXPECT_EQ(jsonResponse.at("type").as_string(), "response");
EXPECT_EQ(jsonResponse.at("id").as_int64(), 1234);
EXPECT_EQ(jsonResponse.at("api_version").as_int64(), 1);
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio);
});
}

View File

@@ -1,583 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/AsioContextTestFixture.hpp"
#include "util/AssignRandomPort.hpp"
#include "util/LoggerFixtures.hpp"
#include "util/MockPrometheus.hpp"
#include "util/NameGenerator.hpp"
#include "util/Taggable.hpp"
#include "util/TestHttpClient.hpp"
#include "util/TestWebSocketClient.hpp"
#include "util/config/ConfigConstraints.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigFileJson.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/ProcessingPolicy.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/Server.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/address_v4.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/beast/websocket/error.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
#include <cstdint>
#include <optional>
#include <ranges>
#include <string>
using namespace web::ng;
using namespace util::config;
namespace http = boost::beast::http;
struct MakeServerTestBundle {
std::string testName;
std::string configJson;
bool expectSuccess;
};
struct MakeServerTest : NoLoggerFixture, testing::WithParamInterface<MakeServerTestBundle> {
protected:
boost::asio::io_context ioContext_;
};
TEST_P(MakeServerTest, Make)
{
ConfigFileJson const json{boost::json::parse(GetParam().configJson).as_object()};
util::config::ClioConfigDefinition config{
{"server.ip", ConfigValue{ConfigType::String}.optional()},
{"server.port", ConfigValue{ConfigType::Integer}.optional()},
{"server.processing_policy", ConfigValue{ConfigType::String}.defaultValue("parallel")},
{"server.parallel_requests_limit", ConfigValue{ConfigType::Integer}.optional()},
{"server.ws_max_sending_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(1500)},
{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
{"ssl_cert_file", ConfigValue{ConfigType::String}.optional()},
{"ssl_key_file", ConfigValue{ConfigType::String}.optional()}
};
auto const errors = config.parse(json);
ASSERT_TRUE(!errors.has_value());
auto const expectedServer =
makeServer(config, [](auto&&) -> std::expected<void, Response> { return {}; }, [](auto&&) {}, ioContext_);
EXPECT_EQ(expectedServer.has_value(), GetParam().expectSuccess);
}
INSTANTIATE_TEST_CASE_P(
MakeServerTests,
MakeServerTest,
testing::Values(
MakeServerTestBundle{
"BadEndpoint",
R"JSON(
{
"server": {"ip": "wrong", "port": 12345}
}
)JSON",
false
},
MakeServerTestBundle{
"BadSslConfig",
R"JSON(
{
"server": {"ip": "127.0.0.1", "port": 12345},
"ssl_cert_file": "some_file"
}
)JSON",
false
},
MakeServerTestBundle{
"BadProcessingPolicy",
R"JSON(
{
"server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "wrong"}
}
)JSON",
false
},
MakeServerTestBundle{
"CorrectConfig_ParallelPolicy",
R"JSON(
{
"server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "parallel"}
}
)JSON",
true
},
MakeServerTestBundle{
"CorrectConfig_SequentPolicy",
R"JSON(
{
"server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "sequent"}
}
)JSON",
true
}
),
tests::util::kNAME_GENERATOR
);
struct ServerTest : util::prometheus::WithPrometheus, SyncAsioContextTest {
ServerTest()
{
[&]() { ASSERT_TRUE(server_.has_value()); }();
server_->onGet("/", getHandler_.AsStdFunction());
server_->onPost("/", postHandler_.AsStdFunction());
server_->onWs(wsHandler_.AsStdFunction());
}
protected:
uint32_t const serverPort_ = tests::util::generateFreePort();
ClioConfigDefinition const config_{
{"server.ip", ConfigValue{ConfigType::String}.defaultValue("127.0.0.1").withConstraint(gValidateIp)},
{"server.port", ConfigValue{ConfigType::Integer}.defaultValue(serverPort_).withConstraint(gValidatePort)},
{"server.processing_policy", ConfigValue{ConfigType::String}.defaultValue("parallel")},
{"server.admin_password", ConfigValue{ConfigType::String}.optional()},
{"server.local_admin", ConfigValue{ConfigType::Boolean}.optional()},
{"server.parallel_requests_limit", ConfigValue{ConfigType::Integer}.optional()},
{"server.ws_max_sending_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(1500)},
{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
{"ssl_key_file", ConfigValue{ConfigType::String}.optional()},
{"ssl_cert_file", ConfigValue{ConfigType::String}.optional()}
};
Server::OnConnectCheck emptyOnConnectCheck_ = [](auto&&) -> std::expected<void, Response> { return {}; };
std::expected<Server, std::string> server_ = makeServer(config_, emptyOnConnectCheck_, [](auto&&) {}, ctx_);
std::string requestMessage_ = "some request";
std::string const headerName_ = "Some-header";
std::string const headerValue_ = "some value";
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
getHandler_;
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
postHandler_;
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
wsHandler_;
};
TEST_F(ServerTest, BadEndpoint)
{
boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::address_v4::from_string("1.2.3.4"), 0};
util::TagDecoratorFactory const tagDecoratorFactory{
ClioConfigDefinition{{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}}
};
Server server{
ctx_,
endpoint,
std::nullopt,
ProcessingPolicy::Sequential,
std::nullopt,
tagDecoratorFactory,
std::nullopt,
emptyOnConnectCheck_,
[](auto&&) {}
};
auto maybeError = server.run();
ASSERT_TRUE(maybeError.has_value());
EXPECT_THAT(*maybeError, testing::HasSubstr("Error creating TCP acceptor"));
}
struct ServerHttpTestBundle {
std::string testName;
http::verb method;
Request::Method
expectedMethod() const
{
switch (method) {
case http::verb::get:
return Request::Method::Get;
case http::verb::post:
return Request::Method::Post;
default:
return Request::Method::Unsupported;
}
}
};
struct ServerHttpTest : ServerTest, testing::WithParamInterface<ServerHttpTestBundle> {};
TEST_F(ServerHttpTest, ClientDisconnects)
{
HttpAsyncClient client{ctx_};
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
client.disconnect();
ctx_.stop();
});
server_->run();
runContext();
}
TEST_F(ServerHttpTest, OnConnectCheck)
{
auto const serverPort = tests::util::generateFreePort();
boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::address_v4::from_string("0.0.0.0"), serverPort};
util::TagDecoratorFactory const tagDecoratorFactory{
ClioConfigDefinition{{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}}
};
testing::StrictMock<testing::MockFunction<std::expected<void, Response>(Connection const&)>> onConnectCheck;
Server server{
ctx_,
endpoint,
std::nullopt,
ProcessingPolicy::Sequential,
std::nullopt,
tagDecoratorFactory,
std::nullopt,
onConnectCheck.AsStdFunction(),
[](auto&&) {}
};
HttpAsyncClient client{ctx_};
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
boost::asio::steady_timer timer{yield.get_executor()};
EXPECT_CALL(onConnectCheck, Call)
.WillOnce([&timer](Connection const& connection) -> std::expected<void, Response> {
EXPECT_EQ(connection.ip(), "127.0.0.1");
timer.cancel();
return {};
});
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
// Have to send a request here because the server does async_detect_ssl() which waits for some data to appear
client.send(
http::request<http::string_body>{http::verb::get, "/", 11, requestMessage_},
yield,
std::chrono::milliseconds{100}
);
// Wait for the onConnectCheck to be called
timer.expires_after(std::chrono::milliseconds{100});
boost::system::error_code error; // Unused
timer.async_wait(yield[error]);
client.gracefulShutdown();
ctx_.stop();
});
server.run();
runContext();
}
TEST_F(ServerHttpTest, OnConnectCheckFailed)
{
auto const serverPort = tests::util::generateFreePort();
boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::address_v4::from_string("0.0.0.0"), serverPort};
util::TagDecoratorFactory const tagDecoratorFactory{
ClioConfigDefinition{{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}}
};
testing::StrictMock<testing::MockFunction<std::expected<void, Response>(Connection const&)>> onConnectCheck;
Server server{
ctx_,
endpoint,
std::nullopt,
ProcessingPolicy::Sequential,
std::nullopt,
tagDecoratorFactory,
std::nullopt,
onConnectCheck.AsStdFunction(),
[](auto&&) {}
};
HttpAsyncClient client{ctx_};
EXPECT_CALL(onConnectCheck, Call).WillOnce([](Connection const& connection) {
EXPECT_EQ(connection.ip(), "127.0.0.1");
return std::unexpected{
Response{http::status::too_many_requests, boost::json::object{{"error", "some error"}}, connection}
};
});
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
// Have to send a request here because the server does async_detect_ssl() which waits for some data to appear
client.send(
http::request<http::string_body>{http::verb::get, "/", 11, requestMessage_},
yield,
std::chrono::milliseconds{100}
);
auto const response = client.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(response.has_value()) << response.error().message(); }();
EXPECT_EQ(response->result(), http::status::too_many_requests);
EXPECT_EQ(response->body(), R"JSON({"error":"some error"})JSON");
EXPECT_EQ(response->version(), 11);
client.gracefulShutdown();
ctx_.stop();
});
server.run();
runContext();
}
TEST_F(ServerHttpTest, OnDisconnectHook)
{
auto const serverPort = tests::util::generateFreePort();
boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::address_v4::from_string("0.0.0.0"), serverPort};
util::TagDecoratorFactory const tagDecoratorFactory{
ClioConfigDefinition{{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}}
};
testing::StrictMock<testing::MockFunction<void(Connection const&)>> onDisconnectHookMock;
Server server{
ctx_,
endpoint,
std::nullopt,
ProcessingPolicy::Sequential,
std::nullopt,
tagDecoratorFactory,
std::nullopt,
emptyOnConnectCheck_,
onDisconnectHookMock.AsStdFunction()
};
HttpAsyncClient client{ctx_};
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
boost::asio::steady_timer timer{ctx_.get_executor(), std::chrono::milliseconds{100}};
EXPECT_CALL(onDisconnectHookMock, Call).WillOnce([&timer](auto&&) { timer.cancel(); });
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
client.send(
http::request<http::string_body>{http::verb::get, "/", 11, requestMessage_},
yield,
std::chrono::milliseconds{100}
);
client.gracefulShutdown();
// Wait for OnDisconnectHook is called
boost::system::error_code error;
timer.async_wait(yield[error]);
ctx_.stop();
});
server.run();
runContext();
}
TEST_F(ServerHttpTest, ClientIsDisconnectedIfServerStopped)
{
HttpAsyncClient client{ctx_};
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
// Have to send a request here because the server does async_detect_ssl() which waits for some data to appear
maybeError = client.send(
http::request<http::string_body>{http::verb::get, "/", 11, requestMessage_},
yield,
std::chrono::milliseconds{100}
);
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
auto message = client.receive(yield, std::chrono::milliseconds{100});
EXPECT_TRUE(message.has_value()) << message.error().message();
EXPECT_EQ(message->result(), http::status::service_unavailable);
EXPECT_EQ(message->body(), "This Clio node is shutting down. Please try another node.");
ctx_.stop();
});
server_->run();
runSyncOperation([this](auto yield) { server_->stop(yield); });
runContext();
}
TEST_P(ServerHttpTest, RequestResponse)
{
HttpAsyncClient client{ctx_};
http::request<http::string_body> request{GetParam().method, "/", 11, requestMessage_};
request.set(headerName_, headerValue_);
Response const response{http::status::ok, "some response", Request{request}};
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
for ([[maybe_unused]] auto i : std::ranges::iota_view{0, 3}) {
maybeError = client.send(request, yield, std::chrono::milliseconds{100});
EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
auto const expectedResponse = client.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }();
EXPECT_EQ(expectedResponse->result(), http::status::ok);
EXPECT_EQ(expectedResponse->body(), response.message());
}
client.gracefulShutdown();
ctx_.stop();
});
auto& handler = GetParam().method == http::verb::get ? getHandler_ : postHandler_;
EXPECT_CALL(handler, Call)
.Times(3)
.WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&, auto&&) {
EXPECT_TRUE(receivedRequest.isHttp());
EXPECT_EQ(receivedRequest.method(), GetParam().expectedMethod());
EXPECT_EQ(receivedRequest.message(), request.body());
EXPECT_EQ(receivedRequest.target(), request.target());
EXPECT_EQ(receivedRequest.headerValue(headerName_), request.at(headerName_));
return response;
});
server_->run();
runContext();
}
INSTANTIATE_TEST_SUITE_P(
ServerHttpTests,
ServerHttpTest,
testing::Values(ServerHttpTestBundle{"GET", http::verb::get}, ServerHttpTestBundle{"POST", http::verb::post}),
tests::util::kNAME_GENERATOR
);
TEST_F(ServerTest, WsClientDisconnects)
{
WebSocketAsyncClient client{ctx_};
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
client.close();
ctx_.stop();
});
server_->run();
runContext();
}
TEST_F(ServerTest, WsRequestResponse)
{
WebSocketAsyncClient client{ctx_};
Request::HttpHeaders const headers{};
Response const response{http::status::ok, "some response", Request{requestMessage_, headers}};
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
for ([[maybe_unused]] auto i : std::ranges::iota_view{0, 3}) {
maybeError = client.send(yield, requestMessage_, std::chrono::milliseconds{100});
EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
auto const expectedResponse = client.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }();
EXPECT_EQ(expectedResponse.value(), response.message());
}
client.gracefulClose(yield, std::chrono::milliseconds{100});
ctx_.stop();
});
EXPECT_CALL(wsHandler_, Call)
.Times(3)
.WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&, auto&&) {
EXPECT_FALSE(receivedRequest.isHttp());
EXPECT_EQ(receivedRequest.method(), Request::Method::Websocket);
EXPECT_EQ(receivedRequest.message(), requestMessage_);
EXPECT_EQ(receivedRequest.target(), std::nullopt);
return response;
});
server_->run();
runContext();
}
TEST_F(ServerTest, WsClientIsDisconnectedIfServerStopped)
{
WebSocketAsyncClient client{ctx_};
boost::asio::spawn(ctx_, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
EXPECT_TRUE(maybeError.has_value());
EXPECT_EQ(maybeError.value().value(), static_cast<int>(boost::beast::websocket::error::upgrade_declined));
ctx_.stop();
});
server_->run();
runSyncOperation([this](auto yield) { server_->stop(yield); });
runContext();
}

View File

@@ -1,172 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/AsioContextTestFixture.hpp"
#include "util/Taggable.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/SubscriptionContext.hpp"
#include "web/ng/impl/MockWsConnection.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/system/errc.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <cstddef>
#include <memory>
#include <optional>
#include <string>
using namespace web::ng;
using namespace util::config;
struct NgSubscriptionContextTests : SyncAsioContextTest {
SubscriptionContext
makeSubscriptionContext(boost::asio::yield_context yield, std::optional<size_t> maxSendQueueSize = std::nullopt)
{
return SubscriptionContext{tagFactory_, connection_, maxSendQueueSize, yield, errorHandler_.AsStdFunction()};
}
protected:
util::TagDecoratorFactory tagFactory_{ClioConfigDefinition{
{"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
}};
MockWsConnectionImpl connection_{"some ip", boost::beast::flat_buffer{}, tagFactory_};
testing::StrictMock<testing::MockFunction<bool(web::ng::Error const&, Connection const&)>> errorHandler_;
};
TEST_F(NgSubscriptionContextTests, Send)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message = std::make_shared<std::string>("some message");
EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto&&) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
return std::nullopt;
});
subscriptionContext.send(message);
subscriptionContext.disconnect(yield);
});
}
TEST_F(NgSubscriptionContextTests, SendOrder)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message1 = std::make_shared<std::string>("message1");
auto const message2 = std::make_shared<std::string>("message2");
testing::Sequence const sequence;
EXPECT_CALL(connection_, sendBuffer)
.InSequence(sequence)
.WillOnce([&message1](boost::asio::const_buffer buffer, auto&&) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message1);
return std::nullopt;
});
EXPECT_CALL(connection_, sendBuffer)
.InSequence(sequence)
.WillOnce([&message2](boost::asio::const_buffer buffer, auto&&) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message2);
return std::nullopt;
});
subscriptionContext.send(message1);
subscriptionContext.send(message2);
subscriptionContext.disconnect(yield);
});
}
TEST_F(NgSubscriptionContextTests, SendFailed)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message = std::make_shared<std::string>("some message");
EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto&&) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
return boost::system::errc::make_error_code(boost::system::errc::not_supported);
});
EXPECT_CALL(errorHandler_, Call).WillOnce(testing::Return(true));
EXPECT_CALL(connection_, close);
subscriptionContext.send(message);
subscriptionContext.disconnect(yield);
});
}
TEST_F(NgSubscriptionContextTests, SendTooManySubscriptions)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield, 1);
auto const message = std::make_shared<std::string>("message1");
EXPECT_CALL(connection_, sendBuffer)
.WillOnce([&message](boost::asio::const_buffer buffer, boost::asio::yield_context innerYield) {
boost::asio::post(innerYield); // simulate send is slow by switching to another coroutine
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
return std::nullopt;
});
EXPECT_CALL(connection_, close);
subscriptionContext.send(message);
subscriptionContext.send(message);
subscriptionContext.send(message);
subscriptionContext.disconnect(yield);
});
}
TEST_F(NgSubscriptionContextTests, SendAfterDisconnect)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message = std::make_shared<std::string>("some message");
subscriptionContext.disconnect(yield);
subscriptionContext.send(message);
});
}
TEST_F(NgSubscriptionContextTests, OnDisconnect)
{
testing::StrictMock<testing::MockFunction<void(web::SubscriptionContextInterface*)>> onDisconnect;
runSpawn([&](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
subscriptionContext.onDisconnect(onDisconnect.AsStdFunction());
EXPECT_CALL(onDisconnect, Call(&subscriptionContext));
subscriptionContext.disconnect(yield);
});
}
TEST_F(NgSubscriptionContextTests, SetApiSubversion)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
subscriptionContext.setApiSubversion(42);
EXPECT_EQ(subscriptionContext.apiSubversion(), 42);
});
}

View File

@@ -1,358 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "rpc/Errors.hpp"
#include "util/LoggerFixtures.hpp"
#include "util/NameGenerator.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/impl/ErrorHandling.hpp"
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <gtest/gtest.h>
#include <optional>
#include <string>
#include <utility>
#include <variant>
using namespace web::ng::impl;
using namespace web::ng;
namespace http = boost::beast::http;
struct NgErrorHandlingTests : NoLoggerFixture {
static Request
makeRequest(bool isHttp, std::optional<std::string> body = std::nullopt)
{
if (isHttp)
return Request{http::request<http::string_body>{http::verb::post, "/", 11, body.value_or("")}};
static Request::HttpHeaders const kHEADERS;
return Request{body.value_or(""), kHEADERS};
}
};
struct NgErrorHandlingMakeErrorTestBundle {
std::string testName;
bool isHttp;
rpc::Status status;
std::string expectedMessage;
boost::beast::http::status expectedStatus;
};
struct NgErrorHandlingMakeErrorTest : NgErrorHandlingTests,
testing::WithParamInterface<NgErrorHandlingMakeErrorTestBundle> {};
TEST_P(NgErrorHandlingMakeErrorTest, MakeError)
{
auto const request = makeRequest(GetParam().isHttp);
ErrorHelper const errorHelper{request};
auto response = errorHelper.makeError(GetParam().status);
EXPECT_EQ(response.message(), GetParam().expectedMessage);
if (GetParam().isHttp) {
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), GetParam().expectedStatus);
std::string expectedContentType = "text/html";
if (std::holds_alternative<rpc::RippledError>(GetParam().status.code))
expectedContentType = "application/json";
EXPECT_EQ(httpResponse.at(http::field::content_type), expectedContentType);
}
}
INSTANTIATE_TEST_CASE_P(
ng_ErrorHandlingMakeErrorTestGroup,
NgErrorHandlingMakeErrorTest,
testing::ValuesIn({
NgErrorHandlingMakeErrorTestBundle{
"WsRequest",
false,
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON",
boost::beast::http::status::ok
},
NgErrorHandlingMakeErrorTestBundle{
"HttpRequest_InvalidApiVersion",
true,
rpc::Status{rpc::ClioError::RpcInvalidApiVersion},
"invalid_API_version",
boost::beast::http::status::bad_request
},
NgErrorHandlingMakeErrorTestBundle{
"HttpRequest_CommandIsMissing",
true,
rpc::Status{rpc::ClioError::RpcCommandIsMissing},
"Null method",
boost::beast::http::status::bad_request
},
NgErrorHandlingMakeErrorTestBundle{
"HttpRequest_CommandIsEmpty",
true,
rpc::Status{rpc::ClioError::RpcCommandIsEmpty},
"method is empty",
boost::beast::http::status::bad_request
},
NgErrorHandlingMakeErrorTestBundle{
"HttpRequest_CommandNotString",
true,
rpc::Status{rpc::ClioError::RpcCommandNotString},
"method is not string",
boost::beast::http::status::bad_request
},
NgErrorHandlingMakeErrorTestBundle{
"HttpRequest_ParamsUnparsable",
true,
rpc::Status{rpc::ClioError::RpcParamsUnparsable},
"params unparsable",
boost::beast::http::status::bad_request
},
NgErrorHandlingMakeErrorTestBundle{
"HttpRequest_RippledError",
true,
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
R"JSON({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})JSON",
boost::beast::http::status::bad_request
},
}),
tests::util::kNAME_GENERATOR
);
struct NgErrorHandlingMakeInternalErrorTestBundle {
std::string testName;
bool isHttp;
std::optional<std::string> request;
boost::json::object expectedResult;
};
struct NgErrorHandlingMakeInternalErrorTest : NgErrorHandlingTests,
testing::WithParamInterface<NgErrorHandlingMakeInternalErrorTestBundle> {
};
TEST_P(NgErrorHandlingMakeInternalErrorTest, ComposeError)
{
auto const request = makeRequest(GetParam().isHttp, GetParam().request);
std::optional<boost::json::object> const requestJson = GetParam().request.has_value()
? std::make_optional(boost::json::parse(*GetParam().request).as_object())
: std::nullopt;
ErrorHelper const errorHelper{request, requestJson};
auto response = errorHelper.makeInternalError();
EXPECT_EQ(response.message(), boost::json::serialize(GetParam().expectedResult));
if (GetParam().isHttp) {
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::internal_server_error);
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
}
}
INSTANTIATE_TEST_CASE_P(
ng_ErrorHandlingComposeErrorTestGroup,
NgErrorHandlingMakeInternalErrorTest,
testing::ValuesIn(
{NgErrorHandlingMakeInternalErrorTestBundle{
"NoRequest_WebsocketConnection",
false,
std::nullopt,
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"}}
},
NgErrorHandlingMakeInternalErrorTestBundle{
"NoRequest_HttpConnection",
true,
std::nullopt,
{{"result",
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"}}}}
},
NgErrorHandlingMakeInternalErrorTestBundle{
"Request_WebsocketConnection",
false,
std::string{R"JSON({"id": 1, "api_version": 2})JSON"},
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"},
{"id", 1},
{"api_version", 2},
{"request", {{"id", 1}, {"api_version", 2}}}}
},
NgErrorHandlingMakeInternalErrorTestBundle{
"Request_WebsocketConnection_NoId",
false,
std::string{R"JSON({"api_version": 2})JSON"},
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"},
{"api_version", 2},
{"request", {{"api_version", 2}}}}
},
NgErrorHandlingMakeInternalErrorTestBundle{
"Request_HttpConnection",
true,
std::string{R"JSON({"id": 1, "api_version": 2})JSON"},
{{"result",
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"},
{"id", 1},
{"request", {{"id", 1}, {"api_version", 2}}}}}}
}}
),
tests::util::kNAME_GENERATOR
);
TEST_F(NgErrorHandlingTests, MakeNotReadyError)
{
auto const request = makeRequest(true);
auto response = ErrorHelper{request}.makeNotReadyError();
EXPECT_EQ(
response.message(),
std::string{
R"JSON({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})JSON"
}
);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
}
TEST_F(NgErrorHandlingTests, MakeTooBusyError_WebsocketRequest)
{
auto const request = makeRequest(false);
auto response = ErrorHelper{request}.makeTooBusyError();
EXPECT_EQ(
response.message(),
std::string{
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
}
);
}
TEST_F(NgErrorHandlingTests, sendTooBusyError_HttpConnection)
{
auto const request = makeRequest(true);
auto response = ErrorHelper{request}.makeTooBusyError();
EXPECT_EQ(
response.message(),
std::string{
R"JSON({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})JSON"
}
);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::service_unavailable);
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
}
TEST_F(NgErrorHandlingTests, makeJsonParsingError_WebsocketConnection)
{
auto const request = makeRequest(false);
auto response = ErrorHelper{request}.makeJsonParsingError();
EXPECT_EQ(
response.message(),
std::string{
R"JSON({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})JSON"
}
);
}
TEST_F(NgErrorHandlingTests, makeJsonParsingError_HttpConnection)
{
auto const request = makeRequest(true);
auto response = ErrorHelper{request}.makeJsonParsingError();
EXPECT_EQ(response.message(), std::string{"Unable to parse JSON from the request"});
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::bad_request);
EXPECT_EQ(httpResponse.at(http::field::content_type), "text/html");
}
struct NgErrorHandlingComposeErrorTestBundle {
std::string testName;
bool isHttp;
std::optional<boost::json::object> request;
std::string expectedMessage;
};
struct NgErrorHandlingComposeErrorTest : NgErrorHandlingTests,
testing::WithParamInterface<NgErrorHandlingComposeErrorTestBundle> {};
TEST_P(NgErrorHandlingComposeErrorTest, ComposeError)
{
auto const request = makeRequest(GetParam().isHttp);
ErrorHelper const errorHelper{request, GetParam().request};
auto const response = errorHelper.composeError(rpc::Status{rpc::RippledError::rpcINTERNAL});
EXPECT_EQ(boost::json::serialize(response), GetParam().expectedMessage);
}
INSTANTIATE_TEST_CASE_P(
ng_ErrorHandlingComposeErrorTestGroup,
NgErrorHandlingComposeErrorTest,
testing::ValuesIn(
{NgErrorHandlingComposeErrorTestBundle{
"NoRequest_WebsocketConnection",
false,
std::nullopt,
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})JSON"
},
NgErrorHandlingComposeErrorTestBundle{
"NoRequest_HttpConnection",
true,
std::nullopt,
R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"}})JSON"
},
NgErrorHandlingComposeErrorTestBundle{
"Request_WebsocketConnection",
false,
boost::json::object{{"id", 1}, {"api_version", 2}},
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"api_version":2,"request":{"id":1,"api_version":2}})JSON",
},
NgErrorHandlingComposeErrorTestBundle{
"Request_WebsocketConnection_NoId",
false,
boost::json::object{{"api_version", 2}},
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","api_version":2,"request":{"api_version":2}})JSON",
},
NgErrorHandlingComposeErrorTestBundle{
"Request_HttpConnection",
true,
boost::json::object{{"id", 1}, {"api_version", 2}},
R"JSON({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"request":{"id":1,"api_version":2}}})JSON"
}}
),
tests::util::kNAME_GENERATOR
);