mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-28 15:45:52 +00:00
@@ -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
|
||||
|
||||
@@ -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 :
|
||||
|
||||
@@ -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), ());
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
Reference in New Issue
Block a user