mirror of
https://github.com/XRPLF/clio.git
synced 2026-04-29 15:37:53 +00:00
feat: Integrate new webserver (#1722)
For #919. The new web server is not using dosguard yet. It will be fixed by a separate PR.
This commit is contained in:
@@ -77,7 +77,8 @@
|
||||
// send a reply for each request whenever it is ready.
|
||||
"parallel_requests_limit": 10, // Optional parameter, used only if "processing_strategy" is "parallel". It limits the number of requests for one client connection processed in parallel. Infinite if not specified.
|
||||
// Max number of responses to queue up before sent successfully. If a client's waiting queue is too long, the server will close the connection.
|
||||
"ws_max_sending_queue_size": 1500
|
||||
"ws_max_sending_queue_size": 1500,
|
||||
"__ng_web_server": false // Use ng web server. This is a temporary setting which will be deleted after switching to ng web server
|
||||
},
|
||||
// Time in seconds for graceful shutdown. Defaults to 10 seconds. Not fully implemented yet.
|
||||
"graceful_period": 10.0,
|
||||
|
||||
@@ -44,6 +44,7 @@ CliArgs::parse(int argc, char const* argv[])
|
||||
("help,h", "print help message and exit")
|
||||
("version,v", "print version and exit")
|
||||
("conf,c", po::value<std::string>()->default_value(defaultConfigPath), "configuration file")
|
||||
("ng-web-server,w", "Use ng-web-server")
|
||||
;
|
||||
// clang-format on
|
||||
po::positional_options_description positional;
|
||||
@@ -64,7 +65,8 @@ CliArgs::parse(int argc, char const* argv[])
|
||||
}
|
||||
|
||||
auto configPath = parsed["conf"].as<std::string>();
|
||||
return Action{Action::Run{std::move(configPath)}};
|
||||
return Action{Action::Run{.configPath = std::move(configPath), .useNgWebServer = parsed.count("ng-web-server") != 0}
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
||||
@@ -43,14 +43,13 @@ public:
|
||||
public:
|
||||
/** @brief Run action. */
|
||||
struct Run {
|
||||
/** @brief Configuration file path. */
|
||||
std::string configPath;
|
||||
std::string configPath; ///< Configuration file path.
|
||||
bool useNgWebServer; ///< Whether to use a ng web server
|
||||
};
|
||||
|
||||
/** @brief Exit action. */
|
||||
struct Exit {
|
||||
/** @brief Exit code. */
|
||||
int exitCode;
|
||||
int exitCode; ///< Exit code.
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,25 +26,39 @@
|
||||
#include "etl/NetworkValidatedLedgers.hpp"
|
||||
#include "feed/SubscriptionManager.hpp"
|
||||
#include "rpc/Counters.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/RPCEngine.hpp"
|
||||
#include "rpc/WorkQueue.hpp"
|
||||
#include "rpc/common/impl/HandlerProvider.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/build/Build.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "util/prometheus/Http.hpp"
|
||||
#include "util/prometheus/Prometheus.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/RPCServerHandler.hpp"
|
||||
#include "web/Server.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuard.hpp"
|
||||
#include "web/dosguard/IntervalSweepHandler.hpp"
|
||||
#include "web/dosguard/WhitelistHandler.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/RPCServerHandler.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/spawn.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace app {
|
||||
@@ -79,7 +93,7 @@ ClioApplication::ClioApplication(util::Config const& config) : config_(config),
|
||||
}
|
||||
|
||||
int
|
||||
ClioApplication::run()
|
||||
ClioApplication::run(bool const useNgWebServer)
|
||||
{
|
||||
auto const threads = config_.valueOr("io_threads", 2);
|
||||
if (threads <= 0) {
|
||||
@@ -126,9 +140,91 @@ ClioApplication::run()
|
||||
auto const rpcEngine =
|
||||
RPCEngineType::make_RPCEngine(config_, backend, balancer, dosGuard, workQueue, counters, handlerProvider);
|
||||
|
||||
if (useNgWebServer or config_.valueOr("server.__ng_web_server", false)) {
|
||||
web::ng::RPCServerHandler<RPCEngineType, etl::ETLService> handler{config_, backend, rpcEngine, etl};
|
||||
|
||||
auto expectedAdminVerifier = web::make_AdminVerificationStrategy(config_);
|
||||
if (not expectedAdminVerifier.has_value()) {
|
||||
LOG(util::LogService::error()) << "Error creating admin verifier: " << expectedAdminVerifier.error();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
auto const adminVerifier = std::move(expectedAdminVerifier).value();
|
||||
|
||||
auto httpServer = web::ng::make_Server(config_, ioc);
|
||||
|
||||
if (not httpServer.has_value()) {
|
||||
LOG(util::LogService::error()) << "Error creating web server: " << httpServer.error();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
httpServer->onGet(
|
||||
"/metrics",
|
||||
[adminVerifier](
|
||||
web::ng::Request const& request,
|
||||
web::ng::ConnectionMetadata& connectionMetadata,
|
||||
web::SubscriptionContextPtr,
|
||||
boost::asio::yield_context
|
||||
) -> web::ng::Response {
|
||||
auto const maybeHttpRequest = request.asHttpRequest();
|
||||
ASSERT(maybeHttpRequest.has_value(), "Got not a http request in Get");
|
||||
auto const& httpRequest = maybeHttpRequest->get();
|
||||
|
||||
// FIXME(#1702): Using veb server thread to handle prometheus request. Better to post on work queue.
|
||||
auto maybeResponse = util::prometheus::handlePrometheusRequest(
|
||||
httpRequest, adminVerifier->isAdmin(httpRequest, connectionMetadata.ip())
|
||||
);
|
||||
ASSERT(maybeResponse.has_value(), "Got unexpected request for Prometheus");
|
||||
return web::ng::Response{std::move(maybeResponse).value(), request};
|
||||
}
|
||||
);
|
||||
|
||||
util::Logger webServerLog{"WebServer"};
|
||||
auto onRequest = [adminVerifier, &webServerLog, &handler](
|
||||
web::ng::Request const& request,
|
||||
web::ng::ConnectionMetadata& connectionMetadata,
|
||||
web::SubscriptionContextPtr subscriptionContext,
|
||||
boost::asio::yield_context yield
|
||||
) -> web::ng::Response {
|
||||
LOG(webServerLog.info()) << connectionMetadata.tag()
|
||||
<< "Received request from ip = " << connectionMetadata.ip()
|
||||
<< " - posting to WorkQueue";
|
||||
|
||||
connectionMetadata.setIsAdmin([&adminVerifier, &request, &connectionMetadata]() {
|
||||
return adminVerifier->isAdmin(request.httpHeaders(), connectionMetadata.ip());
|
||||
});
|
||||
|
||||
try {
|
||||
return handler(request, connectionMetadata, std::move(subscriptionContext), yield);
|
||||
} catch (std::exception const&) {
|
||||
return web::ng::Response{
|
||||
boost::beast::http::status::internal_server_error,
|
||||
rpc::makeError(rpc::RippledError::rpcINTERNAL),
|
||||
request
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
httpServer->onPost("/", onRequest);
|
||||
httpServer->onWs(onRequest);
|
||||
|
||||
auto const maybeError = httpServer->run();
|
||||
if (maybeError.has_value()) {
|
||||
LOG(util::LogService::error()) << "Error starting web server: " << *maybeError;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Blocks until stopped.
|
||||
// When stopped, shared_ptrs fall out of scope
|
||||
// Calls destructors on all resources, and destructs in order
|
||||
start(ioc, threads);
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
// Init the web server
|
||||
auto handler =
|
||||
std::make_shared<web::RPCServerHandler<RPCEngineType, etl::ETLService>>(config_, backend, rpcEngine, etl);
|
||||
|
||||
auto const httpServer = web::make_HttpServer(config_, ioc, dosGuard, handler);
|
||||
|
||||
// Blocks until stopped.
|
||||
|
||||
@@ -42,10 +42,12 @@ public:
|
||||
/**
|
||||
* @brief Run the application
|
||||
*
|
||||
* @param useNgWebServer Whether to use the new web server
|
||||
*
|
||||
* @return exit code
|
||||
*/
|
||||
int
|
||||
run();
|
||||
run(bool useNgWebServer);
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
||||
@@ -19,12 +19,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace feed {
|
||||
using Subscriber = web::ConnectionBase;
|
||||
|
||||
using Subscriber = web::SubscriptionContextInterface;
|
||||
using SubscriberPtr = Subscriber*;
|
||||
using SubscriberSharedPtr = std::shared_ptr<Subscriber>;
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ ProposedTransactionFeed::sub(SubscriberSharedPtr const& subscriber)
|
||||
if (added) {
|
||||
LOG(logger_.info()) << subscriber->tag() << "Subscribed tx_proposed";
|
||||
++subAllCount_.get();
|
||||
subscriber->onDisconnect.connect([this](SubscriberPtr connection) { unsubInternal(connection); });
|
||||
subscriber->onDisconnect([this](SubscriberPtr connection) { unsubInternal(connection); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,9 +73,7 @@ ProposedTransactionFeed::sub(ripple::AccountID const& account, SubscriberSharedP
|
||||
if (added) {
|
||||
LOG(logger_.info()) << subscriber->tag() << "Subscribed accounts_proposed " << account;
|
||||
++subAccountCount_.get();
|
||||
subscriber->onDisconnect.connect([this, account](SubscriberPtr connection) {
|
||||
unsubInternal(account, connection);
|
||||
});
|
||||
subscriber->onDisconnect([this, account](SubscriberPtr connection) { unsubInternal(account, connection); });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ SingleFeedBase::sub(SubscriberSharedPtr const& subscriber)
|
||||
if (added) {
|
||||
LOG(logger_.info()) << subscriber->tag() << "Subscribed " << name_;
|
||||
++subCount_.get();
|
||||
subscriber->onDisconnect.connect([this](SubscriberPtr connectionDisconnecting) {
|
||||
subscriber->onDisconnect([this](SubscriberPtr connectionDisconnecting) {
|
||||
unsubInternal(connectionDisconnecting);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#include <boost/signals2.hpp>
|
||||
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
@@ -53,14 +53,14 @@ namespace feed::impl {
|
||||
void
|
||||
TransactionFeed::TransactionSlot::operator()(AllVersionTransactionsType const& allVersionMsgs) const
|
||||
{
|
||||
if (auto connection = connectionWeakPtr.lock(); connection) {
|
||||
if (auto connection = subscriptionContextWeakPtr.lock(); connection) {
|
||||
// Check if this connection already sent
|
||||
if (feed.get().notified_.contains(connection.get()))
|
||||
return;
|
||||
|
||||
feed.get().notified_.insert(connection.get());
|
||||
|
||||
if (connection->apiSubVersion < 2u) {
|
||||
if (connection->apiSubversion() < 2u) {
|
||||
connection->send(allVersionMsgs[0]);
|
||||
return;
|
||||
}
|
||||
@@ -75,7 +75,7 @@ TransactionFeed::sub(SubscriberSharedPtr const& subscriber)
|
||||
if (added) {
|
||||
LOG(logger_.info()) << subscriber->tag() << "Subscribed transactions";
|
||||
++subAllCount_.get();
|
||||
subscriber->onDisconnect.connect([this](SubscriberPtr connection) { unsubInternal(connection); });
|
||||
subscriber->onDisconnect([this](SubscriberPtr connection) { unsubInternal(connection); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,18 +86,16 @@ TransactionFeed::sub(ripple::AccountID const& account, SubscriberSharedPtr const
|
||||
if (added) {
|
||||
LOG(logger_.info()) << subscriber->tag() << "Subscribed account " << account;
|
||||
++subAccountCount_.get();
|
||||
subscriber->onDisconnect.connect([this, account](SubscriberPtr connection) {
|
||||
unsubInternal(account, connection);
|
||||
});
|
||||
subscriber->onDisconnect([this, account](SubscriberPtr connection) { unsubInternal(account, connection); });
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
TransactionFeed::subProposed(SubscriberSharedPtr const& subscriber)
|
||||
{
|
||||
auto const added = txProposedsignal_.connectTrackableSlot(subscriber, TransactionSlot(*this, subscriber));
|
||||
auto const added = txProposedSignal_.connectTrackableSlot(subscriber, TransactionSlot(*this, subscriber));
|
||||
if (added) {
|
||||
subscriber->onDisconnect.connect([this](SubscriberPtr connection) { unsubProposedInternal(connection); });
|
||||
subscriber->onDisconnect([this](SubscriberPtr connection) { unsubProposedInternal(connection); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +105,7 @@ TransactionFeed::subProposed(ripple::AccountID const& account, SubscriberSharedP
|
||||
auto const added =
|
||||
accountProposedSignal_.connectTrackableSlot(subscriber, account, TransactionSlot(*this, subscriber));
|
||||
if (added) {
|
||||
subscriber->onDisconnect.connect([this, account](SubscriberPtr connection) {
|
||||
subscriber->onDisconnect([this, account](SubscriberPtr connection) {
|
||||
unsubProposedInternal(account, connection);
|
||||
});
|
||||
}
|
||||
@@ -120,7 +118,7 @@ TransactionFeed::sub(ripple::Book const& book, SubscriberSharedPtr const& subscr
|
||||
if (added) {
|
||||
LOG(logger_.info()) << subscriber->tag() << "Subscribed book " << book;
|
||||
++subBookCount_.get();
|
||||
subscriber->onDisconnect.connect([this, book](SubscriberPtr connection) { unsubInternal(book, connection); });
|
||||
subscriber->onDisconnect([this, book](SubscriberPtr connection) { unsubInternal(book, connection); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +283,7 @@ TransactionFeed::pub(
|
||||
// clear the notified set. If the same connection subscribes both transactions + proposed_transactions,
|
||||
// rippled SENDS the same message twice
|
||||
notified_.clear();
|
||||
txProposedsignal_.emit(allVersionsMsgs);
|
||||
txProposedSignal_.emit(allVersionsMsgs);
|
||||
notified_.clear();
|
||||
// check duplicate for account and proposed_account, this prevents sending the same message multiple times
|
||||
// if it affects multiple accounts watched by the same connection
|
||||
@@ -323,7 +321,7 @@ TransactionFeed::unsubInternal(ripple::AccountID const& account, SubscriberPtr s
|
||||
void
|
||||
TransactionFeed::unsubProposedInternal(SubscriberPtr subscriber)
|
||||
{
|
||||
txProposedsignal_.disconnect(subscriber);
|
||||
txProposedSignal_.disconnect(subscriber);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -52,10 +52,10 @@ class TransactionFeed {
|
||||
|
||||
struct TransactionSlot {
|
||||
std::reference_wrapper<TransactionFeed> feed;
|
||||
std::weak_ptr<Subscriber> connectionWeakPtr;
|
||||
std::weak_ptr<Subscriber> subscriptionContextWeakPtr;
|
||||
|
||||
TransactionSlot(TransactionFeed& feed, SubscriberSharedPtr const& connection)
|
||||
: feed(feed), connectionWeakPtr(connection)
|
||||
: feed(feed), subscriptionContextWeakPtr(connection)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ class TransactionFeed {
|
||||
|
||||
// Signals for proposed tx subscribers
|
||||
TrackableSignalMap<ripple::AccountID, Subscriber, AllVersionTransactionsType const&> accountProposedSignal_;
|
||||
TrackableSignal<Subscriber, AllVersionTransactionsType const&> txProposedsignal_;
|
||||
TrackableSignal<Subscriber, AllVersionTransactionsType const&> txProposedSignal_;
|
||||
|
||||
std::unordered_set<SubscriberPtr>
|
||||
notified_; // Used by slots to prevent double notifications if tx contains multiple subscribed accounts
|
||||
|
||||
@@ -44,7 +44,8 @@ try {
|
||||
}
|
||||
util::LogService::init(config);
|
||||
app::ClioApplication clio{config};
|
||||
return clio.run();
|
||||
|
||||
return clio.run(run.useNgWebServer);
|
||||
}
|
||||
);
|
||||
} catch (std::exception const& e) {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/Context.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json/array.hpp>
|
||||
@@ -34,8 +35,8 @@
|
||||
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
using namespace std;
|
||||
using namespace util;
|
||||
@@ -46,11 +47,12 @@ std::expected<web::Context, Status>
|
||||
make_WsContext(
|
||||
boost::asio::yield_context yc,
|
||||
boost::json::object const& request,
|
||||
std::shared_ptr<web::ConnectionBase> const& session,
|
||||
web::SubscriptionContextPtr session,
|
||||
util::TagDecoratorFactory const& tagFactory,
|
||||
data::LedgerRange const& range,
|
||||
std::string const& clientIp,
|
||||
std::reference_wrapper<APIVersionParser const> apiVersionParser
|
||||
std::reference_wrapper<APIVersionParser const> apiVersionParser,
|
||||
bool isAdmin
|
||||
)
|
||||
{
|
||||
boost::json::value commandValue = nullptr;
|
||||
@@ -68,7 +70,7 @@ make_WsContext(
|
||||
return Error{{ClioError::rpcINVALID_API_VERSION, apiVersion.error()}};
|
||||
|
||||
auto const command = boost::json::value_to<std::string>(commandValue);
|
||||
return web::Context(yc, command, *apiVersion, request, session, tagFactory, range, clientIp, session->isAdmin());
|
||||
return web::Context(yc, command, *apiVersion, request, std::move(session), tagFactory, range, clientIp, isAdmin);
|
||||
}
|
||||
|
||||
std::expected<web::Context, Status>
|
||||
@@ -94,7 +96,7 @@ make_HttpContext(
|
||||
auto const command = boost::json::value_to<std::string>(request.at("method"));
|
||||
|
||||
if (command == "subscribe" || command == "unsubscribe")
|
||||
return Error{{RippledError::rpcBAD_SYNTAX, "Subscribe and unsubscribe are only allowed or websocket."}};
|
||||
return Error{{RippledError::rpcBAD_SYNTAX, "Subscribe and unsubscribe are only allowed for websocket."}};
|
||||
|
||||
if (!request.at("params").is_array())
|
||||
return Error{{ClioError::rpcPARAMS_UNPARSEABLE, "Missing params array."}};
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#include "rpc/common/APIVersion.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/Context.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json.hpp>
|
||||
@@ -32,7 +32,6 @@
|
||||
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
/*
|
||||
@@ -49,22 +48,24 @@ namespace rpc {
|
||||
*
|
||||
* @param yc The coroutine context
|
||||
* @param request The request as JSON object
|
||||
* @param session The connection
|
||||
* @param session The subscription context
|
||||
* @param tagFactory A factory that provides tags to track requests
|
||||
* @param range The ledger range that is available at request time
|
||||
* @param clientIp The IP address of the connected client
|
||||
* @param apiVersionParser A parser that is used to parse out the "api_version" field
|
||||
* @param isAdmin Whether the request has admin privileges
|
||||
* @return A Websocket context or error Status
|
||||
*/
|
||||
std::expected<web::Context, Status>
|
||||
make_WsContext(
|
||||
boost::asio::yield_context yc,
|
||||
boost::json::object const& request,
|
||||
std::shared_ptr<web::ConnectionBase> const& session,
|
||||
web::SubscriptionContextPtr session,
|
||||
util::TagDecoratorFactory const& tagFactory,
|
||||
data::LedgerRange const& range,
|
||||
std::string const& clientIp,
|
||||
std::reference_wrapper<APIVersionParser const> apiVersionParser
|
||||
std::reference_wrapper<APIVersionParser const> apiVersionParser,
|
||||
bool isAdmin
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json/array.hpp>
|
||||
@@ -32,7 +33,6 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
@@ -117,7 +117,7 @@ struct VoidOutput {};
|
||||
*/
|
||||
struct Context {
|
||||
boost::asio::yield_context yield;
|
||||
std::shared_ptr<web::ConnectionBase> session = {}; // NOLINT(readability-redundant-member-init)
|
||||
web::SubscriptionContextPtr session = {}; // NOLINT(readability-redundant-member-init)
|
||||
bool isAdmin = false;
|
||||
std::string clientIp = {}; // NOLINT(readability-redundant-member-init)
|
||||
uint32_t apiVersion = 0u; // invalid by default
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "data/Types.hpp"
|
||||
#include "feed/SubscriptionManagerInterface.hpp"
|
||||
#include "feed/Types.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
@@ -114,7 +115,7 @@ SubscribeHandler::process(Input input, Context const& ctx) const
|
||||
auto output = Output{};
|
||||
|
||||
// Mimic rippled. No matter what the request is, the api version changes for the whole session
|
||||
ctx.session->apiSubVersion = ctx.apiVersion;
|
||||
ctx.session->setApiSubversion(ctx.apiVersion);
|
||||
|
||||
if (input.streams) {
|
||||
auto const ledger = subscribeToStreams(ctx.yield, *(input.streams), ctx.session);
|
||||
@@ -138,7 +139,7 @@ boost::json::object
|
||||
SubscribeHandler::subscribeToStreams(
|
||||
boost::asio::yield_context yield,
|
||||
std::vector<std::string> const& streams,
|
||||
std::shared_ptr<web::ConnectionBase> const& session
|
||||
feed::SubscriberSharedPtr const& session
|
||||
) const
|
||||
{
|
||||
auto response = boost::json::object{};
|
||||
@@ -165,7 +166,7 @@ SubscribeHandler::subscribeToStreams(
|
||||
void
|
||||
SubscribeHandler::subscribeToAccountsProposed(
|
||||
std::vector<std::string> const& accounts,
|
||||
std::shared_ptr<web::ConnectionBase> const& session
|
||||
feed::SubscriberSharedPtr const& session
|
||||
) const
|
||||
{
|
||||
for (auto const& account : accounts) {
|
||||
@@ -177,7 +178,7 @@ SubscribeHandler::subscribeToAccountsProposed(
|
||||
void
|
||||
SubscribeHandler::subscribeToAccounts(
|
||||
std::vector<std::string> const& accounts,
|
||||
std::shared_ptr<web::ConnectionBase> const& session
|
||||
feed::SubscriberSharedPtr const& session
|
||||
) const
|
||||
{
|
||||
for (auto const& account : accounts) {
|
||||
@@ -189,7 +190,7 @@ SubscribeHandler::subscribeToAccounts(
|
||||
void
|
||||
SubscribeHandler::subscribeToBooks(
|
||||
std::vector<OrderBook> const& books,
|
||||
std::shared_ptr<web::ConnectionBase> const& session,
|
||||
feed::SubscriberSharedPtr const& session,
|
||||
boost::asio::yield_context yield,
|
||||
Output& output
|
||||
) const
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "feed/SubscriptionManagerInterface.hpp"
|
||||
#include "feed/Types.hpp"
|
||||
#include "rpc/common/Specs.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
|
||||
@@ -128,23 +129,20 @@ private:
|
||||
subscribeToStreams(
|
||||
boost::asio::yield_context yield,
|
||||
std::vector<std::string> const& streams,
|
||||
std::shared_ptr<web::ConnectionBase> const& session
|
||||
feed::SubscriberSharedPtr const& session
|
||||
) const;
|
||||
|
||||
void
|
||||
subscribeToAccounts(std::vector<std::string> const& accounts, std::shared_ptr<web::ConnectionBase> const& session)
|
||||
subscribeToAccounts(std::vector<std::string> const& accounts, feed::SubscriberSharedPtr const& session) const;
|
||||
|
||||
void
|
||||
subscribeToAccountsProposed(std::vector<std::string> const& accounts, feed::SubscriberSharedPtr const& session)
|
||||
const;
|
||||
|
||||
void
|
||||
subscribeToAccountsProposed(
|
||||
std::vector<std::string> const& accounts,
|
||||
std::shared_ptr<web::ConnectionBase> const& session
|
||||
) const;
|
||||
|
||||
void
|
||||
subscribeToBooks(
|
||||
std::vector<OrderBook> const& books,
|
||||
std::shared_ptr<web::ConnectionBase> const& session,
|
||||
feed::SubscriberSharedPtr const& session,
|
||||
boost::asio::yield_context yield,
|
||||
Output& output
|
||||
) const;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "feed/SubscriptionManagerInterface.hpp"
|
||||
#include "feed/Types.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
@@ -106,10 +107,11 @@ UnsubscribeHandler::process(Input input, Context const& ctx) const
|
||||
|
||||
return Output{};
|
||||
}
|
||||
|
||||
void
|
||||
UnsubscribeHandler::unsubscribeFromStreams(
|
||||
std::vector<std::string> const& streams,
|
||||
std::shared_ptr<web::ConnectionBase> const& session
|
||||
feed::SubscriberSharedPtr const& session
|
||||
) const
|
||||
{
|
||||
for (auto const& stream : streams) {
|
||||
@@ -130,21 +132,21 @@ UnsubscribeHandler::unsubscribeFromStreams(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
UnsubscribeHandler::unsubscribeFromAccounts(
|
||||
std::vector<std::string> accounts,
|
||||
std::shared_ptr<web::ConnectionBase> const& session
|
||||
) const
|
||||
UnsubscribeHandler::unsubscribeFromAccounts(std::vector<std::string> accounts, feed::SubscriberSharedPtr const& session)
|
||||
const
|
||||
{
|
||||
for (auto const& account : accounts) {
|
||||
auto const accountID = accountFromStringStrict(account);
|
||||
subscriptions_->unsubAccount(*accountID, session);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
UnsubscribeHandler::unsubscribeFromProposedAccounts(
|
||||
std::vector<std::string> accountsProposed,
|
||||
std::shared_ptr<web::ConnectionBase> const& session
|
||||
feed::SubscriberSharedPtr const& session
|
||||
) const
|
||||
{
|
||||
for (auto const& account : accountsProposed) {
|
||||
@@ -153,10 +155,8 @@ UnsubscribeHandler::unsubscribeFromProposedAccounts(
|
||||
}
|
||||
}
|
||||
void
|
||||
UnsubscribeHandler::unsubscribeFromBooks(
|
||||
std::vector<OrderBook> const& books,
|
||||
std::shared_ptr<web::ConnectionBase> const& session
|
||||
) const
|
||||
UnsubscribeHandler::unsubscribeFromBooks(std::vector<OrderBook> const& books, feed::SubscriberSharedPtr const& session)
|
||||
const
|
||||
{
|
||||
for (auto const& orderBook : books) {
|
||||
subscriptions_->unsubBook(orderBook.book, session);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "feed/SubscriptionManagerInterface.hpp"
|
||||
#include "feed/Types.hpp"
|
||||
#include "rpc/common/Specs.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
|
||||
@@ -105,22 +106,17 @@ public:
|
||||
|
||||
private:
|
||||
void
|
||||
unsubscribeFromStreams(std::vector<std::string> const& streams, std::shared_ptr<web::ConnectionBase> const& session)
|
||||
unsubscribeFromStreams(std::vector<std::string> const& streams, feed::SubscriberSharedPtr const& session) const;
|
||||
|
||||
void
|
||||
unsubscribeFromAccounts(std::vector<std::string> accounts, feed::SubscriberSharedPtr const& session) const;
|
||||
|
||||
void
|
||||
unsubscribeFromProposedAccounts(std::vector<std::string> accountsProposed, feed::SubscriberSharedPtr const& session)
|
||||
const;
|
||||
|
||||
void
|
||||
unsubscribeFromAccounts(std::vector<std::string> accounts, std::shared_ptr<web::ConnectionBase> const& session)
|
||||
const;
|
||||
|
||||
void
|
||||
unsubscribeFromProposedAccounts(
|
||||
std::vector<std::string> accountsProposed,
|
||||
std::shared_ptr<web::ConnectionBase> const& session
|
||||
) const;
|
||||
|
||||
void
|
||||
unsubscribeFromBooks(std::vector<OrderBook> const& books, std::shared_ptr<web::ConnectionBase> const& session)
|
||||
const;
|
||||
unsubscribeFromBooks(std::vector<OrderBook> const& books, feed::SubscriberSharedPtr const& session) const;
|
||||
|
||||
/**
|
||||
* @brief Convert a JSON object to an Input
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
namespace util {
|
||||
|
||||
CoroutineGroup::CoroutineGroup(boost::asio::yield_context yield, std::optional<int> maxChildren)
|
||||
CoroutineGroup::CoroutineGroup(boost::asio::yield_context yield, std::optional<size_t> maxChildren)
|
||||
: timer_{yield.get_executor(), boost::asio::steady_timer::duration::max()}, maxChildren_{maxChildren}
|
||||
{
|
||||
}
|
||||
@@ -41,28 +41,30 @@ CoroutineGroup::~CoroutineGroup()
|
||||
ASSERT(childrenCounter_ == 0, "CoroutineGroup is destroyed without waiting for child coroutines to finish");
|
||||
}
|
||||
|
||||
bool
|
||||
CoroutineGroup::canSpawn() const
|
||||
{
|
||||
return not maxChildren_.has_value() or childrenCounter_ < *maxChildren_;
|
||||
}
|
||||
|
||||
bool
|
||||
CoroutineGroup::spawn(boost::asio::yield_context yield, std::function<void(boost::asio::yield_context)> fn)
|
||||
{
|
||||
if (not canSpawn())
|
||||
if (isFull())
|
||||
return false;
|
||||
|
||||
++childrenCounter_;
|
||||
boost::asio::spawn(yield, [this, fn = std::move(fn)](boost::asio::yield_context yield) {
|
||||
fn(yield);
|
||||
--childrenCounter_;
|
||||
if (childrenCounter_ == 0)
|
||||
timer_.cancel();
|
||||
onCoroutineCompleted();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<std::function<void()>>
|
||||
CoroutineGroup::registerForeign()
|
||||
{
|
||||
if (isFull())
|
||||
return std::nullopt;
|
||||
|
||||
++childrenCounter_;
|
||||
return [this]() { onCoroutineCompleted(); };
|
||||
}
|
||||
|
||||
void
|
||||
CoroutineGroup::asyncWait(boost::asio::yield_context yield)
|
||||
{
|
||||
@@ -79,4 +81,20 @@ CoroutineGroup::size() const
|
||||
return childrenCounter_;
|
||||
}
|
||||
|
||||
bool
|
||||
CoroutineGroup::isFull() const
|
||||
{
|
||||
return maxChildren_.has_value() && childrenCounter_ >= *maxChildren_;
|
||||
}
|
||||
|
||||
void
|
||||
CoroutineGroup::onCoroutineCompleted()
|
||||
{
|
||||
ASSERT(childrenCounter_ != 0, "onCoroutineCompleted() called more times than the number of child coroutines");
|
||||
|
||||
--childrenCounter_;
|
||||
if (childrenCounter_ == 0)
|
||||
timer_.cancel();
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
@@ -31,11 +32,12 @@ namespace util {
|
||||
/**
|
||||
* @brief CoroutineGroup is a helper class to manage a group of coroutines. It allows to spawn multiple coroutines and
|
||||
* wait for all of them to finish.
|
||||
* @note This class is safe to use from multiple threads.
|
||||
*/
|
||||
class CoroutineGroup {
|
||||
boost::asio::steady_timer timer_;
|
||||
std::optional<int> maxChildren_;
|
||||
int childrenCounter_{0};
|
||||
std::optional<size_t> maxChildren_;
|
||||
std::atomic_size_t childrenCounter_{0};
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -45,7 +47,7 @@ public:
|
||||
* @param maxChildren The maximum number of coroutines that can be spawned at the same time. If not provided, there
|
||||
* is no limit
|
||||
*/
|
||||
CoroutineGroup(boost::asio::yield_context yield, std::optional<int> maxChildren = std::nullopt);
|
||||
CoroutineGroup(boost::asio::yield_context yield, std::optional<size_t> maxChildren = std::nullopt);
|
||||
|
||||
/**
|
||||
* @brief Destroy the Coroutine Group object
|
||||
@@ -54,14 +56,6 @@ public:
|
||||
*/
|
||||
~CoroutineGroup();
|
||||
|
||||
/**
|
||||
* @brief Check if a new coroutine can be spawned (i.e. there is space for a new coroutine in the group)
|
||||
*
|
||||
* @return true If a new coroutine can be spawned. false if the maximum number of coroutines has been reached
|
||||
*/
|
||||
bool
|
||||
canSpawn() const;
|
||||
|
||||
/**
|
||||
* @brief Spawn a new coroutine in the group
|
||||
*
|
||||
@@ -74,6 +68,16 @@ public:
|
||||
bool
|
||||
spawn(boost::asio::yield_context yield, std::function<void(boost::asio::yield_context)> fn);
|
||||
|
||||
/**
|
||||
* @brief Register a foreign coroutine this group should wait for.
|
||||
* @note A foreign coroutine is still counted as a child one, i.e. calling this method increases the size of the
|
||||
* group.
|
||||
*
|
||||
* @return A callback to call on foreign coroutine completes or std::nullopt if the group is already full.
|
||||
*/
|
||||
std::optional<std::function<void()>>
|
||||
registerForeign();
|
||||
|
||||
/**
|
||||
* @brief Wait for all the coroutines in the group to finish
|
||||
*
|
||||
@@ -91,6 +95,18 @@ public:
|
||||
*/
|
||||
size_t
|
||||
size() const;
|
||||
|
||||
/**
|
||||
* @brief Check if the group is full
|
||||
*
|
||||
* @return true If the group is full false otherwise
|
||||
*/
|
||||
bool
|
||||
isFull() const;
|
||||
|
||||
private:
|
||||
void
|
||||
onCoroutineCompleted();
|
||||
};
|
||||
|
||||
} // namespace util
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "web/impl/AdminVerificationStrategy.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
|
||||
#include "util/JsonUtils.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
@@ -33,10 +33,10 @@
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace web::impl {
|
||||
namespace web {
|
||||
|
||||
bool
|
||||
IPAdminVerificationStrategy::isAdmin(RequestType const&, std::string_view ip) const
|
||||
IPAdminVerificationStrategy::isAdmin(RequestHeader const&, std::string_view ip) const
|
||||
{
|
||||
return ip == "127.0.0.1";
|
||||
}
|
||||
@@ -54,7 +54,7 @@ PasswordAdminVerificationStrategy::PasswordAdminVerificationStrategy(std::string
|
||||
}
|
||||
|
||||
bool
|
||||
PasswordAdminVerificationStrategy::isAdmin(RequestType const& request, std::string_view) const
|
||||
PasswordAdminVerificationStrategy::isAdmin(RequestHeader const& request, std::string_view) const
|
||||
{
|
||||
auto it = request.find(boost::beast::http::field::authorization);
|
||||
if (it == request.end()) {
|
||||
@@ -81,19 +81,21 @@ make_AdminVerificationStrategy(std::optional<std::string> password)
|
||||
}
|
||||
|
||||
std::expected<std::shared_ptr<AdminVerificationStrategy>, std::string>
|
||||
make_AdminVerificationStrategy(util::Config const& serverConfig)
|
||||
make_AdminVerificationStrategy(util::Config const& config)
|
||||
{
|
||||
auto adminPassword = serverConfig.maybeValue<std::string>("admin_password");
|
||||
auto const localAdmin = serverConfig.maybeValue<bool>("local_admin");
|
||||
bool const localAdminEnabled = localAdmin && localAdmin.value();
|
||||
auto adminPassword = config.maybeValue<std::string>("server.admin_password");
|
||||
auto const localAdmin = config.maybeValue<bool>("server.local_admin");
|
||||
|
||||
if (localAdminEnabled == adminPassword.has_value()) {
|
||||
if (adminPassword.has_value())
|
||||
return std::unexpected{"Admin config error, local_admin and admin_password can not be set together."};
|
||||
return std::unexpected{"Admin config error, either local_admin and admin_password must be specified."};
|
||||
if (adminPassword.has_value() and localAdmin.has_value() and *localAdmin)
|
||||
return std::unexpected{"Admin config error: 'local_admin' and admin_password can not be set together."};
|
||||
|
||||
if (localAdmin.has_value() and !*localAdmin and !adminPassword.has_value()) {
|
||||
return std::unexpected{
|
||||
"Admin config error: either 'local_admin' should be enabled or 'admin_password' must be specified."
|
||||
};
|
||||
}
|
||||
|
||||
return make_AdminVerificationStrategy(std::move(adminPassword));
|
||||
}
|
||||
|
||||
} // namespace web::impl
|
||||
} // namespace web
|
||||
@@ -31,11 +31,14 @@
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace web::impl {
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief Interface for admin verification strategies.
|
||||
*/
|
||||
class AdminVerificationStrategy {
|
||||
public:
|
||||
using RequestType = boost::beast::http::request<boost::beast::http::string_body>;
|
||||
using RequestHeader = boost::beast::http::request<boost::beast::http::string_body>::header_type;
|
||||
virtual ~AdminVerificationStrategy() = default;
|
||||
|
||||
/**
|
||||
@@ -46,9 +49,12 @@ public:
|
||||
* @return true if authorized; false otherwise
|
||||
*/
|
||||
virtual bool
|
||||
isAdmin(RequestType const& request, std::string_view ip) const = 0;
|
||||
isAdmin(RequestHeader const& request, std::string_view ip) const = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Admin verification strategy that checks the ip address of the client.
|
||||
*/
|
||||
class IPAdminVerificationStrategy : public AdminVerificationStrategy {
|
||||
public:
|
||||
/**
|
||||
@@ -59,16 +65,27 @@ public:
|
||||
* @return true if authorized; false otherwise
|
||||
*/
|
||||
bool
|
||||
isAdmin(RequestType const&, std::string_view ip) const override;
|
||||
isAdmin(RequestHeader const&, std::string_view ip) const override;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Admin verification strategy that checks the password from the request header.
|
||||
*/
|
||||
class PasswordAdminVerificationStrategy : public AdminVerificationStrategy {
|
||||
private:
|
||||
std::string passwordSha256_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief The prefix for the password in the request header.
|
||||
*/
|
||||
static constexpr std::string_view passwordPrefix = "Password ";
|
||||
|
||||
/**
|
||||
* @brief Construct a new PasswordAdminVerificationStrategy object
|
||||
*
|
||||
* @param password The password to check
|
||||
*/
|
||||
PasswordAdminVerificationStrategy(std::string const& password);
|
||||
|
||||
/**
|
||||
@@ -79,13 +96,26 @@ public:
|
||||
* @return true if the password from request matches admin password from config
|
||||
*/
|
||||
bool
|
||||
isAdmin(RequestType const& request, std::string_view) const override;
|
||||
isAdmin(RequestHeader const& request, std::string_view) const override;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Factory function for creating an admin verification strategy.
|
||||
*
|
||||
* @param password The optional password to check.
|
||||
* @return Admin verification strategy. If password is provided, it will be PasswordAdminVerificationStrategy.
|
||||
* Otherwise, it will be IPAdminVerificationStrategy.
|
||||
*/
|
||||
std::shared_ptr<AdminVerificationStrategy>
|
||||
make_AdminVerificationStrategy(std::optional<std::string> password);
|
||||
|
||||
/**
|
||||
* @brief Factory function for creating an admin verification strategy from server config.
|
||||
*
|
||||
* @param serverConfig The clio config.
|
||||
* @return Admin verification strategy according to the config or an error message.
|
||||
*/
|
||||
std::expected<std::shared_ptr<AdminVerificationStrategy>, std::string>
|
||||
make_AdminVerificationStrategy(util::Config const& serverConfig);
|
||||
|
||||
} // namespace web::impl
|
||||
} // namespace web
|
||||
@@ -2,18 +2,21 @@ add_library(clio_web)
|
||||
|
||||
target_sources(
|
||||
clio_web
|
||||
PRIVATE Resolver.cpp
|
||||
PRIVATE AdminVerificationStrategy.cpp
|
||||
dosguard/DOSGuard.cpp
|
||||
dosguard/IntervalSweepHandler.cpp
|
||||
dosguard/WhitelistHandler.cpp
|
||||
impl/AdminVerificationStrategy.cpp
|
||||
ng/Connection.cpp
|
||||
ng/impl/ErrorHandling.cpp
|
||||
ng/impl/ConnectionHandler.cpp
|
||||
ng/impl/ServerSslContext.cpp
|
||||
ng/impl/WsConnection.cpp
|
||||
ng/Server.cpp
|
||||
ng/Request.cpp
|
||||
ng/Response.cpp
|
||||
ng/Server.cpp
|
||||
ng/SubscriptionContext.cpp
|
||||
Resolver.cpp
|
||||
SubscriptionContext.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(clio_web PUBLIC clio_util)
|
||||
|
||||
@@ -22,14 +22,13 @@
|
||||
#include "data/Types.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
@@ -43,7 +42,7 @@ struct Context : util::Taggable {
|
||||
std::string method;
|
||||
std::uint32_t apiVersion;
|
||||
boost::json::object params;
|
||||
std::shared_ptr<web::ConnectionBase> session;
|
||||
SubscriptionContextPtr session;
|
||||
data::LedgerRange range;
|
||||
std::string clientIp;
|
||||
bool isAdmin;
|
||||
@@ -55,7 +54,7 @@ struct Context : util::Taggable {
|
||||
* @param command The method/command requested
|
||||
* @param apiVersion The api_version parsed from the request
|
||||
* @param params Request's parameters/data as a JSON object
|
||||
* @param session The connection to the peer
|
||||
* @param subscriptionContext The subscription context of the connection
|
||||
* @param tagFactory A factory that is used to generate tags to track requests and connections
|
||||
* @param range The ledger range that is available at the time of the request
|
||||
* @param clientIp IP of the peer
|
||||
@@ -66,7 +65,7 @@ struct Context : util::Taggable {
|
||||
std::string command,
|
||||
std::uint32_t apiVersion,
|
||||
boost::json::object params,
|
||||
std::shared_ptr<web::ConnectionBase> const& session,
|
||||
SubscriptionContextPtr subscriptionContext,
|
||||
util::TagDecoratorFactory const& tagFactory,
|
||||
data::LedgerRange const& range,
|
||||
std::string clientIp,
|
||||
@@ -77,7 +76,7 @@ struct Context : util::Taggable {
|
||||
, method(std::move(command))
|
||||
, apiVersion(apiVersion)
|
||||
, params(std::move(params))
|
||||
, session(session)
|
||||
, session(std::move(subscriptionContext))
|
||||
, range(range)
|
||||
, clientIp(std::move(clientIp))
|
||||
, isAdmin(isAdmin)
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/PlainWsSession.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/impl/HttpBase.hpp"
|
||||
#include "web/interface/Concepts.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
@@ -71,7 +73,7 @@ public:
|
||||
explicit HttpSession(
|
||||
tcp::socket&& socket,
|
||||
std::string const& ip,
|
||||
std::shared_ptr<impl::AdminVerificationStrategy> const& adminVerification,
|
||||
std::shared_ptr<AdminVerificationStrategy> const& adminVerification,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> const& handler,
|
||||
|
||||
@@ -161,11 +161,12 @@ private:
|
||||
return rpc::make_WsContext(
|
||||
yield,
|
||||
request,
|
||||
connection,
|
||||
connection->makeSubscriptionContext(tagFactory_),
|
||||
tagFactory_.with(connection->tag()),
|
||||
*range,
|
||||
connection->clientIp,
|
||||
std::cref(apiVersionParser_)
|
||||
std::cref(apiVersionParser_),
|
||||
connection->isAdmin()
|
||||
);
|
||||
}
|
||||
return rpc::make_HttpContext(
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/HttpSession.hpp"
|
||||
#include "web/SslHttpSession.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
@@ -84,7 +85,7 @@ class Detector : public std::enable_shared_from_this<Detector<PlainSessionType,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> const dosGuard_;
|
||||
std::shared_ptr<HandlerType> const handler_;
|
||||
boost::beast::flat_buffer buffer_;
|
||||
std::shared_ptr<impl::AdminVerificationStrategy> const adminVerification_;
|
||||
std::shared_ptr<AdminVerificationStrategy> const adminVerification_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
|
||||
public:
|
||||
@@ -105,7 +106,7 @@ public:
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
std::shared_ptr<HandlerType> handler,
|
||||
std::shared_ptr<impl::AdminVerificationStrategy> adminVerification,
|
||||
std::shared_ptr<AdminVerificationStrategy> adminVerification,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: stream_(std::move(socket))
|
||||
@@ -216,7 +217,7 @@ class Server : public std::enable_shared_from_this<Server<PlainSessionType, SslS
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard_;
|
||||
std::shared_ptr<HandlerType> handler_;
|
||||
tcp::acceptor acceptor_;
|
||||
std::shared_ptr<impl::AdminVerificationStrategy> adminVerification_;
|
||||
std::shared_ptr<AdminVerificationStrategy> adminVerification_;
|
||||
std::uint32_t maxWsSendingQueueSize_;
|
||||
|
||||
public:
|
||||
@@ -229,7 +230,7 @@ public:
|
||||
* @param tagFactory A factory that is used to generate tags to track requests and sessions
|
||||
* @param dosGuard The denial of service guard to use
|
||||
* @param handler The server handler to use
|
||||
* @param adminPassword The optional password to verify admin role in requests
|
||||
* @param adminVerification The admin verification strategy to use
|
||||
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
|
||||
*/
|
||||
Server(
|
||||
@@ -239,7 +240,7 @@ public:
|
||||
util::TagDecoratorFactory tagFactory,
|
||||
dosguard::DOSGuardInterface& dosGuard,
|
||||
std::shared_ptr<HandlerType> handler,
|
||||
std::optional<std::string> adminPassword,
|
||||
std::shared_ptr<AdminVerificationStrategy> adminVerification,
|
||||
std::uint32_t maxWsSendingQueueSize
|
||||
)
|
||||
: ioc_(std::ref(ioc))
|
||||
@@ -248,7 +249,7 @@ public:
|
||||
, dosGuard_(std::ref(dosGuard))
|
||||
, handler_(std::move(handler))
|
||||
, acceptor_(boost::asio::make_strand(ioc))
|
||||
, adminVerification_(impl::make_AdminVerificationStrategy(std::move(adminPassword)))
|
||||
, adminVerification_(std::move(adminVerification))
|
||||
, maxWsSendingQueueSize_(maxWsSendingQueueSize)
|
||||
{
|
||||
boost::beast::error_code ec;
|
||||
@@ -355,20 +356,11 @@ make_HttpServer(
|
||||
auto const serverConfig = config.section("server");
|
||||
auto const address = boost::asio::ip::make_address(serverConfig.value<std::string>("ip"));
|
||||
auto const port = serverConfig.value<unsigned short>("port");
|
||||
auto adminPassword = serverConfig.maybeValue<std::string>("admin_password");
|
||||
auto const localAdmin = serverConfig.maybeValue<bool>("local_admin");
|
||||
|
||||
// Throw config error when localAdmin is true and admin_password is also set
|
||||
if (localAdmin && localAdmin.value() && adminPassword) {
|
||||
LOG(log.error()) << "local_admin is true but admin_password is also set, please specify only one method "
|
||||
"to authorize admin";
|
||||
throw std::logic_error("Admin config error, local_admin and admin_password can not be set together.");
|
||||
}
|
||||
// Throw config error when localAdmin is false but admin_password is not set
|
||||
if (localAdmin && !localAdmin.value() && !adminPassword) {
|
||||
LOG(log.error()) << "local_admin is false but admin_password is not set, please specify one method "
|
||||
"to authorize admin";
|
||||
throw std::logic_error("Admin config error, one method must be specified to authorize admin.");
|
||||
auto expectedAdminVerification = make_AdminVerificationStrategy(config);
|
||||
if (not expectedAdminVerification.has_value()) {
|
||||
LOG(log.error()) << expectedAdminVerification.error();
|
||||
throw std::logic_error{expectedAdminVerification.error()};
|
||||
}
|
||||
|
||||
// If the transactions number is 200 per ledger, A client which subscribes everything will send 400+ feeds for
|
||||
@@ -382,7 +374,7 @@ make_HttpServer(
|
||||
util::TagDecoratorFactory(config),
|
||||
dosGuard,
|
||||
handler,
|
||||
std::move(adminPassword),
|
||||
std::move(expectedAdminVerification).value(),
|
||||
maxWsSendingQueueSize
|
||||
);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/SslWsSession.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/impl/HttpBase.hpp"
|
||||
@@ -79,7 +80,7 @@ public:
|
||||
explicit SslHttpSession(
|
||||
tcp::socket&& socket,
|
||||
std::string const& ip,
|
||||
std::shared_ptr<impl::AdminVerificationStrategy> const& adminVerification,
|
||||
std::shared_ptr<AdminVerificationStrategy> const& adminVerification,
|
||||
boost::asio::ssl::context& ctx,
|
||||
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
|
||||
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
|
||||
|
||||
71
src/web/SubscriptionContext.cpp
Normal file
71
src/web/SubscriptionContext.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 "web/SubscriptionContext.hpp"
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web {
|
||||
|
||||
SubscriptionContext::SubscriptionContext(
|
||||
util::TagDecoratorFactory const& factory,
|
||||
std::shared_ptr<ConnectionBase> connection
|
||||
)
|
||||
: SubscriptionContextInterface{factory}, connection_{connection}
|
||||
{
|
||||
}
|
||||
|
||||
SubscriptionContext::~SubscriptionContext()
|
||||
{
|
||||
onDisconnect_(this);
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionContext::send(std::shared_ptr<std::string> message)
|
||||
{
|
||||
if (auto connection = connection_.lock(); connection != nullptr)
|
||||
connection->send(std::move(message));
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionContext::onDisconnect(OnDisconnectSlot const& slot)
|
||||
{
|
||||
onDisconnect_.connect(slot);
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionContext::setApiSubversion(uint32_t value)
|
||||
{
|
||||
apiSubVersion_ = value;
|
||||
}
|
||||
|
||||
uint32_t
|
||||
SubscriptionContext::apiSubversion() const
|
||||
{
|
||||
return apiSubVersion_;
|
||||
}
|
||||
|
||||
} // namespace web
|
||||
96
src/web/SubscriptionContext.hpp
Normal file
96
src/web/SubscriptionContext.hpp
Normal file
@@ -0,0 +1,96 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/interface/Concepts.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/signals2/variadic_signal.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief A context of a WsBase connection for subscriptions.
|
||||
*/
|
||||
class SubscriptionContext : public SubscriptionContextInterface {
|
||||
std::weak_ptr<ConnectionBase> connection_;
|
||||
boost::signals2::signal<void(SubscriptionContextInterface*)> onDisconnect_;
|
||||
/**
|
||||
* @brief The API version of the web stream client.
|
||||
* This is used to track the api version of this connection, which mainly is used by subscription. It is different
|
||||
* from the api version in Context, which is only used for the current request.
|
||||
*/
|
||||
std::atomic_uint32_t apiSubVersion_ = 0;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Subscription Context object
|
||||
*
|
||||
* @param factory The tag decorator factory to use to init taggable.
|
||||
* @param connection The connection for which the context is created.
|
||||
*/
|
||||
SubscriptionContext(util::TagDecoratorFactory const& factory, std::shared_ptr<ConnectionBase> connection);
|
||||
|
||||
/**
|
||||
* @brief Destroy the Subscription Context object
|
||||
*/
|
||||
~SubscriptionContext() override;
|
||||
|
||||
/**
|
||||
* @brief Send message to the client
|
||||
* @note This method will not do anything if the related connection got disconnected.
|
||||
*
|
||||
* @param message The message to send.
|
||||
*/
|
||||
void
|
||||
send(std::shared_ptr<std::string> message) override;
|
||||
|
||||
/**
|
||||
* @brief Connect a slot to onDisconnect connection signal.
|
||||
*
|
||||
* @param slot The slot to connect.
|
||||
*/
|
||||
void
|
||||
onDisconnect(OnDisconnectSlot const& slot) override;
|
||||
|
||||
/**
|
||||
* @brief Set the API subversion.
|
||||
* @param value The value to set.
|
||||
*/
|
||||
void
|
||||
setApiSubversion(uint32_t value) override;
|
||||
|
||||
/**
|
||||
* @brief Get the API subversion.
|
||||
*
|
||||
* @return The API subversion.
|
||||
*/
|
||||
uint32_t
|
||||
apiSubversion() const override;
|
||||
};
|
||||
|
||||
} // namespace web
|
||||
88
src/web/SubscriptionContextInterface.hpp
Normal file
88
src/web/SubscriptionContextInterface.hpp
Normal file
@@ -0,0 +1,88 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
|
||||
#include <boost/signals2/signal.hpp>
|
||||
#include <boost/signals2/variadic_signal.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace web {
|
||||
|
||||
/**
|
||||
* @brief An interface to provide connection functionality for subscriptions.
|
||||
* @note Since subscription is only allowed for websocket connection, this interface is used only for websocket
|
||||
* connections.
|
||||
*/
|
||||
class SubscriptionContextInterface : public util::Taggable {
|
||||
public:
|
||||
/**
|
||||
* @brief Reusing Taggable constructor
|
||||
*/
|
||||
using util::Taggable::Taggable;
|
||||
|
||||
/**
|
||||
* @brief Send message to the client
|
||||
*
|
||||
* @param message The message to send.
|
||||
*/
|
||||
virtual void
|
||||
send(std::shared_ptr<std::string> message) = 0;
|
||||
|
||||
/**
|
||||
* @brief Alias for on disconnect slot.
|
||||
*/
|
||||
using OnDisconnectSlot = std::function<void(SubscriptionContextInterface*)>;
|
||||
|
||||
/**
|
||||
* @brief Connect a slot to onDisconnect connection signal.
|
||||
*
|
||||
* @param slot The slot to connect.
|
||||
*/
|
||||
virtual void
|
||||
onDisconnect(OnDisconnectSlot const& slot) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set the API subversion.
|
||||
* @param value The value to set.
|
||||
*/
|
||||
virtual void
|
||||
setApiSubversion(uint32_t value) = 0;
|
||||
|
||||
/**
|
||||
* @brief Get the API subversion.
|
||||
*
|
||||
* @return The API subversion.
|
||||
*/
|
||||
virtual uint32_t
|
||||
apiSubversion() const = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief An alias for shared pointer to a SubscriptionContextInterface.
|
||||
*/
|
||||
using SubscriptionContextPtr = std::shared_ptr<SubscriptionContextInterface>;
|
||||
|
||||
} // namespace web
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/build/Build.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "util/prometheus/Http.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/impl/AdminVerificationStrategy.hpp"
|
||||
#include "web/interface/Concepts.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
@@ -275,6 +277,13 @@ public:
|
||||
sender_(httpResponse(status, "application/json", std::move(msg)));
|
||||
}
|
||||
|
||||
SubscriptionContextPtr
|
||||
makeSubscriptionContext(util::TagDecoratorFactory const&) override
|
||||
{
|
||||
ASSERT(false, "SubscriptionContext can't be created for a HTTP connection");
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
void
|
||||
onWrite(bool close, boost::beast::error_code ec, std::size_t bytes_transferred)
|
||||
{
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/SubscriptionContext.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/interface/Concepts.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
@@ -79,6 +81,7 @@ class WsBase : public ConnectionBase, public std::enable_shared_from_this<WsBase
|
||||
std::queue<std::shared_ptr<std::string>> messages_;
|
||||
std::shared_ptr<HandlerType> const handler_;
|
||||
|
||||
SubscriptionContextPtr subscriptionContext_;
|
||||
std::uint32_t maxSendingQueueSize_;
|
||||
|
||||
protected:
|
||||
@@ -184,6 +187,21 @@ public:
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the subscription context for this connection.
|
||||
*
|
||||
* @param factory Tag TagDecoratorFactory to use to create the context.
|
||||
* @return The subscription context for this connection.
|
||||
*/
|
||||
SubscriptionContextPtr
|
||||
makeSubscriptionContext(util::TagDecoratorFactory const& factory) override
|
||||
{
|
||||
if (subscriptionContext_ == nullptr) {
|
||||
subscriptionContext_ = std::make_shared<SubscriptionContext>(factory, shared_from_this());
|
||||
}
|
||||
return subscriptionContext_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a message to the client
|
||||
* @param msg The message to send
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/beast.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
|
||||
@@ -29,6 +27,8 @@
|
||||
|
||||
namespace web {
|
||||
|
||||
struct ConnectionBase;
|
||||
|
||||
/**
|
||||
* @brief Specifies the requirements a Webserver handler must fulfill.
|
||||
*/
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/signals2.hpp>
|
||||
#include <boost/signals2/variadic_signal.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -49,13 +49,6 @@ protected:
|
||||
public:
|
||||
std::string const clientIp;
|
||||
bool upgraded = false;
|
||||
boost::signals2::signal<void(ConnectionBase*)> onDisconnect;
|
||||
/**
|
||||
* @brief The API version of the web stream client.
|
||||
* This is used to track the api version of this connection, which mainly is used by subscription. It is different
|
||||
* from the api version in Context, which is only used for the current request.
|
||||
*/
|
||||
std::uint32_t apiSubVersion = 0;
|
||||
|
||||
/**
|
||||
* @brief Create a new connection base.
|
||||
@@ -68,11 +61,6 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
~ConnectionBase() override
|
||||
{
|
||||
onDisconnect(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Send the response to the client.
|
||||
*
|
||||
@@ -94,6 +82,15 @@ public:
|
||||
throw std::logic_error("web server can not send the shared payload");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the subscription context for this connection.
|
||||
*
|
||||
* @param factory Tag TagDecoratorFactory to use to create the context.
|
||||
* @return The subscription context for this connection.
|
||||
*/
|
||||
virtual SubscriptionContextPtr
|
||||
makeSubscriptionContext(util::TagDecoratorFactory const& factory) = 0;
|
||||
|
||||
/**
|
||||
* @brief Indicates whether the connection had an error and is considered dead.
|
||||
*
|
||||
|
||||
@@ -23,34 +23,34 @@
|
||||
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
ConnectionMetadata::ConnectionMetadata(std::string ip, util::TagDecoratorFactory const& tagDecoratorFactory)
|
||||
: util::Taggable(tagDecoratorFactory), ip_{std::move(ip)}
|
||||
{
|
||||
}
|
||||
|
||||
std::string const&
|
||||
ConnectionMetadata::ip() const
|
||||
{
|
||||
return ip_;
|
||||
}
|
||||
|
||||
bool
|
||||
ConnectionMetadata::isAdmin() const
|
||||
{
|
||||
return isAdmin_.value_or(false);
|
||||
}
|
||||
|
||||
Connection::Connection(
|
||||
std::string ip,
|
||||
boost::beast::flat_buffer buffer,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory
|
||||
)
|
||||
: util::Taggable(tagDecoratorFactory), ip_{std::move(ip)}, buffer_{std::move(buffer)}
|
||||
{
|
||||
}
|
||||
|
||||
ConnectionContext
|
||||
Connection::context() const
|
||||
{
|
||||
return ConnectionContext{*this};
|
||||
}
|
||||
|
||||
std::string const&
|
||||
Connection::ip() const
|
||||
{
|
||||
return ip_;
|
||||
}
|
||||
|
||||
ConnectionContext::ConnectionContext(Connection const& connection) : connection_{connection}
|
||||
: ConnectionMetadata{std::move(ip), tagDecoratorFactory}, buffer_{std::move(buffer)}
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -38,16 +38,67 @@
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
* @brief A forward declaration of ConnectionContext.
|
||||
* @brief An interface for a connection metadata class.
|
||||
*/
|
||||
class ConnectionContext;
|
||||
|
||||
/**
|
||||
*@brief A class representing a connection to a client.
|
||||
*/
|
||||
class Connection : public util::Taggable {
|
||||
class ConnectionMetadata : public util::Taggable {
|
||||
protected:
|
||||
std::string ip_; // client ip
|
||||
std::optional<bool> isAdmin_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new ConnectionMetadata object.
|
||||
*
|
||||
* @param ip The client ip.
|
||||
* @param tagDecoratorFactory The factory for creating tag decorators.
|
||||
*/
|
||||
ConnectionMetadata(std::string ip, util::TagDecoratorFactory const& tagDecoratorFactory);
|
||||
|
||||
/**
|
||||
* @brief Whether the connection was upgraded. Upgraded connections are websocket connections.
|
||||
*
|
||||
* @return true if the connection was upgraded.
|
||||
*/
|
||||
virtual bool
|
||||
wasUpgraded() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Get the ip of the client.
|
||||
*
|
||||
* @return The ip of the client.
|
||||
*/
|
||||
std::string const&
|
||||
ip() const;
|
||||
|
||||
/**
|
||||
* @brief Get whether the client is an admin.
|
||||
*
|
||||
* @return true if the client is an admin.
|
||||
*/
|
||||
bool
|
||||
isAdmin() const;
|
||||
|
||||
/**
|
||||
* @brief Set the isAdmin field.
|
||||
* @note This function is lazy, it will update isAdmin only if it is not set yet.
|
||||
*
|
||||
* @tparam T The invocable type of the function to call to set the isAdmin.
|
||||
* @param setter The function to call to set the isAdmin.
|
||||
*/
|
||||
template <std::invocable T>
|
||||
void
|
||||
setIsAdmin(T&& setter)
|
||||
{
|
||||
if (not isAdmin_.has_value())
|
||||
isAdmin_ = setter();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A class representing a connection to a client.
|
||||
*/
|
||||
class Connection : public ConnectionMetadata {
|
||||
protected:
|
||||
boost::beast::flat_buffer buffer_;
|
||||
|
||||
public:
|
||||
@@ -65,14 +116,6 @@ public:
|
||||
*/
|
||||
Connection(std::string ip, boost::beast::flat_buffer buffer, util::TagDecoratorFactory const& tagDecoratorFactory);
|
||||
|
||||
/**
|
||||
* @brief Whether the connection was upgraded. Upgraded connections are websocket connections.
|
||||
*
|
||||
* @return true if the connection was upgraded.
|
||||
*/
|
||||
virtual bool
|
||||
wasUpgraded() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Send a response to the client.
|
||||
*
|
||||
@@ -81,7 +124,6 @@ public:
|
||||
* @param timeout The timeout for the operation.
|
||||
* @return An error if the operation failed or nullopt if it succeeded.
|
||||
*/
|
||||
|
||||
virtual std::optional<Error>
|
||||
send(
|
||||
Response response,
|
||||
@@ -107,22 +149,6 @@ public:
|
||||
*/
|
||||
virtual void
|
||||
close(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT) = 0;
|
||||
|
||||
/**
|
||||
* @brief Get the connection context.
|
||||
*
|
||||
* @return The connection context.
|
||||
*/
|
||||
ConnectionContext
|
||||
context() const;
|
||||
|
||||
/**
|
||||
* @brief Get the ip of the client.
|
||||
*
|
||||
* @return The ip of the client.
|
||||
*/
|
||||
std::string const&
|
||||
ip() const;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -130,19 +156,4 @@ public:
|
||||
*/
|
||||
using ConnectionPtr = std::unique_ptr<Connection>;
|
||||
|
||||
/**
|
||||
* @brief A class representing the context of a connection.
|
||||
*/
|
||||
class ConnectionContext {
|
||||
std::reference_wrapper<Connection const> connection_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new ConnectionContext object.
|
||||
*
|
||||
* @param connection The connection.
|
||||
*/
|
||||
explicit ConnectionContext(Connection const& connection);
|
||||
};
|
||||
|
||||
} // namespace web::ng
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
@@ -32,6 +33,7 @@ namespace web::ng {
|
||||
/**
|
||||
* @brief Handler for messages.
|
||||
*/
|
||||
using MessageHandler = std::function<Response(Request const&, ConnectionContext, boost::asio::yield_context)>;
|
||||
using MessageHandler =
|
||||
std::function<Response(Request const&, ConnectionMetadata&, SubscriptionContextPtr, boost::asio::yield_context)>;
|
||||
|
||||
} // namespace web::ng
|
||||
|
||||
29
src/web/ng/ProcessingPolicy.hpp
Normal file
29
src/web/ng/ProcessingPolicy.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
* @brief Requests processing policy.
|
||||
*/
|
||||
enum class ProcessingPolicy { Sequential, Parallel };
|
||||
|
||||
} // namespace web::ng
|
||||
336
src/web/ng/RPCServerHandler.hpp
Normal file
336
src/web/ng/RPCServerHandler.hpp
Normal file
@@ -0,0 +1,336 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/Factories.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
#include "rpc/common/impl/APIVersionParser.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/CoroutineGroup.hpp"
|
||||
#include "util/JsonUtils.hpp"
|
||||
#include "util/Profiler.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/ErrorHandling.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <ratio>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
* @brief The server handler for RPC requests called by web server.
|
||||
*
|
||||
* Note: see @ref web::SomeServerHandler concept
|
||||
*/
|
||||
template <typename RPCEngineType, typename ETLType>
|
||||
class RPCServerHandler {
|
||||
std::shared_ptr<BackendInterface const> const backend_;
|
||||
std::shared_ptr<RPCEngineType> const rpcEngine_;
|
||||
std::shared_ptr<ETLType const> const etl_;
|
||||
util::TagDecoratorFactory const tagFactory_;
|
||||
rpc::impl::ProductionAPIVersionParser apiVersionParser_; // can be injected if needed
|
||||
|
||||
util::Logger log_{"RPC"};
|
||||
util::Logger perfLog_{"Performance"};
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new server handler.
|
||||
*
|
||||
* @param config Clio config to use
|
||||
* @param backend The backend to use
|
||||
* @param rpcEngine The RPC engine to use
|
||||
* @param etl The ETL to use
|
||||
*/
|
||||
RPCServerHandler(
|
||||
util::Config const& config,
|
||||
std::shared_ptr<BackendInterface const> const& backend,
|
||||
std::shared_ptr<RPCEngineType> const& rpcEngine,
|
||||
std::shared_ptr<ETLType const> const& etl
|
||||
)
|
||||
: backend_(backend)
|
||||
, rpcEngine_(rpcEngine)
|
||||
, etl_(etl)
|
||||
, tagFactory_(config)
|
||||
, apiVersionParser_(config.sectionOr("api_version", {}))
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The callback when server receives a request.
|
||||
*
|
||||
* @param request The request
|
||||
* @param connectionMetadata The connection metadata
|
||||
* @param subscriptionContext The subscription context
|
||||
* @param yield The yield context
|
||||
* @return The response
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
operator()(
|
||||
Request const& request,
|
||||
ConnectionMetadata const& connectionMetadata,
|
||||
SubscriptionContextPtr subscriptionContext,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
std::optional<Response> response;
|
||||
util::CoroutineGroup coroutineGroup{yield, 1};
|
||||
auto const onTaskComplete = coroutineGroup.registerForeign();
|
||||
ASSERT(onTaskComplete.has_value(), "Coroutine group can't be full");
|
||||
|
||||
bool const postSuccessful = rpcEngine_->post(
|
||||
[this,
|
||||
&request,
|
||||
&response,
|
||||
&onTaskComplete = onTaskComplete.value(),
|
||||
&connectionMetadata,
|
||||
subscriptionContext = std::move(subscriptionContext)](boost::asio::yield_context yield) mutable {
|
||||
try {
|
||||
auto parsedRequest = boost::json::parse(request.message()).as_object();
|
||||
LOG(perfLog_.debug()) << connectionMetadata.tag() << "Adding to work queue";
|
||||
|
||||
if (not connectionMetadata.wasUpgraded() and shouldReplaceParams(parsedRequest))
|
||||
parsedRequest[JS(params)] = boost::json::array({boost::json::object{}});
|
||||
|
||||
response = handleRequest(
|
||||
yield, request, std::move(parsedRequest), connectionMetadata, std::move(subscriptionContext)
|
||||
);
|
||||
} catch (boost::system::system_error const& ex) {
|
||||
// system_error thrown when json parsing failed
|
||||
rpcEngine_->notifyBadSyntax();
|
||||
response = impl::ErrorHelper{request}.makeJsonParsingError();
|
||||
LOG(log_.warn()) << "Error parsing JSON: " << ex.what() << ". For request: " << request.message();
|
||||
} catch (std::invalid_argument const& ex) {
|
||||
// thrown when json parses something that is not an object at top level
|
||||
rpcEngine_->notifyBadSyntax();
|
||||
LOG(log_.warn()) << "Invalid argument error: " << ex.what()
|
||||
<< ". For request: " << request.message();
|
||||
response = impl::ErrorHelper{request}.makeJsonParsingError();
|
||||
} catch (std::exception const& ex) {
|
||||
LOG(perfLog_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
|
||||
rpcEngine_->notifyInternalError();
|
||||
response = impl::ErrorHelper{request}.makeInternalError();
|
||||
}
|
||||
|
||||
// notify the coroutine group that the foreign task is done
|
||||
onTaskComplete();
|
||||
},
|
||||
connectionMetadata.ip()
|
||||
);
|
||||
|
||||
if (not postSuccessful) {
|
||||
// onTaskComplete must be called to notify coroutineGroup that the foreign task is done
|
||||
onTaskComplete->operator()();
|
||||
rpcEngine_->notifyTooBusy();
|
||||
return impl::ErrorHelper{request}.makeTooBusyError();
|
||||
}
|
||||
|
||||
// Put the coroutine to sleep until the foreign task is done
|
||||
coroutineGroup.asyncWait(yield);
|
||||
ASSERT(response.has_value(), "Woke up coroutine without setting response");
|
||||
return std::move(response).value();
|
||||
}
|
||||
|
||||
private:
|
||||
Response
|
||||
handleRequest(
|
||||
boost::asio::yield_context yield,
|
||||
Request const& rawRequest,
|
||||
boost::json::object&& request,
|
||||
ConnectionMetadata const& connectionMetadata,
|
||||
SubscriptionContextPtr subscriptionContext
|
||||
)
|
||||
{
|
||||
LOG(log_.info()) << connectionMetadata.tag() << (connectionMetadata.wasUpgraded() ? "ws" : "http")
|
||||
<< " received request from work queue: " << util::removeSecret(request)
|
||||
<< " ip = " << connectionMetadata.ip();
|
||||
|
||||
try {
|
||||
auto const range = backend_->fetchLedgerRange();
|
||||
if (!range) {
|
||||
// for error that happened before the handler, we don't attach any warnings
|
||||
rpcEngine_->notifyNotReady();
|
||||
return impl::ErrorHelper{rawRequest, std::move(request)}.makeNotReadyError();
|
||||
}
|
||||
|
||||
auto const context = [&] {
|
||||
if (connectionMetadata.wasUpgraded()) {
|
||||
ASSERT(subscriptionContext != nullptr, "Subscription context must exist for a WS connecton");
|
||||
return rpc::make_WsContext(
|
||||
yield,
|
||||
request,
|
||||
std::move(subscriptionContext),
|
||||
tagFactory_.with(connectionMetadata.tag()),
|
||||
*range,
|
||||
connectionMetadata.ip(),
|
||||
std::cref(apiVersionParser_),
|
||||
connectionMetadata.isAdmin()
|
||||
);
|
||||
}
|
||||
return rpc::make_HttpContext(
|
||||
yield,
|
||||
request,
|
||||
tagFactory_.with(connectionMetadata.tag()),
|
||||
*range,
|
||||
connectionMetadata.ip(),
|
||||
std::cref(apiVersionParser_),
|
||||
connectionMetadata.isAdmin()
|
||||
);
|
||||
}();
|
||||
|
||||
if (!context) {
|
||||
auto const err = context.error();
|
||||
LOG(perfLog_.warn()) << connectionMetadata.tag() << "Could not create Web context: " << err;
|
||||
LOG(log_.warn()) << connectionMetadata.tag() << "Could not create Web context: " << err;
|
||||
|
||||
// we count all those as BadSyntax - as the WS path would.
|
||||
// Although over HTTP these will yield a 400 status with a plain text response (for most).
|
||||
rpcEngine_->notifyBadSyntax();
|
||||
return impl::ErrorHelper(rawRequest, std::move(request)).makeError(err);
|
||||
}
|
||||
|
||||
auto [result, timeDiff] = util::timed([&]() { return rpcEngine_->buildResponse(*context); });
|
||||
|
||||
auto us = std::chrono::duration<int, std::milli>(timeDiff);
|
||||
rpc::logDuration(*context, us);
|
||||
|
||||
boost::json::object response;
|
||||
|
||||
if (auto const status = std::get_if<rpc::Status>(&result.response)) {
|
||||
// note: error statuses are counted/notified in buildResponse itself
|
||||
response = impl::ErrorHelper(rawRequest, request).composeError(*status);
|
||||
auto const responseStr = boost::json::serialize(response);
|
||||
|
||||
LOG(perfLog_.debug()) << context->tag() << "Encountered error: " << responseStr;
|
||||
LOG(log_.debug()) << context->tag() << "Encountered error: " << responseStr;
|
||||
} else {
|
||||
// This can still technically be an error. Clio counts forwarded requests as successful.
|
||||
rpcEngine_->notifyComplete(context->method, us);
|
||||
|
||||
auto& json = std::get<boost::json::object>(result.response);
|
||||
auto const isForwarded =
|
||||
json.contains("forwarded") && json.at("forwarded").is_bool() && json.at("forwarded").as_bool();
|
||||
|
||||
if (isForwarded)
|
||||
json.erase("forwarded");
|
||||
|
||||
// if the result is forwarded - just use it as is
|
||||
// if forwarded request has error, for http, error should be in "result"; for ws, error should
|
||||
// be at top
|
||||
if (isForwarded && (json.contains(JS(result)) || connectionMetadata.wasUpgraded())) {
|
||||
for (auto const& [k, v] : json)
|
||||
response.insert_or_assign(k, v);
|
||||
} else {
|
||||
response[JS(result)] = json;
|
||||
}
|
||||
|
||||
if (isForwarded)
|
||||
response["forwarded"] = true;
|
||||
|
||||
// for ws there is an additional field "status" in the response,
|
||||
// otherwise the "status" is in the "result" field
|
||||
if (connectionMetadata.wasUpgraded()) {
|
||||
auto const appendFieldIfExist = [&](auto const& field) {
|
||||
if (request.contains(field) and not request.at(field).is_null())
|
||||
response[field] = request.at(field);
|
||||
};
|
||||
|
||||
appendFieldIfExist(JS(id));
|
||||
appendFieldIfExist(JS(api_version));
|
||||
|
||||
if (!response.contains(JS(error)))
|
||||
response[JS(status)] = JS(success);
|
||||
|
||||
response[JS(type)] = JS(response);
|
||||
} else {
|
||||
if (response.contains(JS(result)) && !response[JS(result)].as_object().contains(JS(error)))
|
||||
response[JS(result)].as_object()[JS(status)] = JS(success);
|
||||
}
|
||||
}
|
||||
|
||||
boost::json::array warnings = std::move(result.warnings);
|
||||
warnings.emplace_back(rpc::makeWarning(rpc::warnRPC_CLIO));
|
||||
|
||||
if (etl_->lastCloseAgeSeconds() >= 60)
|
||||
warnings.emplace_back(rpc::makeWarning(rpc::warnRPC_OUTDATED));
|
||||
|
||||
response["warnings"] = warnings;
|
||||
return Response{boost::beast::http::status::ok, response, rawRequest};
|
||||
} catch (std::exception const& ex) {
|
||||
// note: while we are catching this in buildResponse too, this is here to make sure
|
||||
// that any other code that may throw is outside of buildResponse is also worked around.
|
||||
LOG(perfLog_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
|
||||
LOG(log_.error()) << connectionMetadata.tag() << "Caught exception: " << ex.what();
|
||||
|
||||
rpcEngine_->notifyInternalError();
|
||||
return impl::ErrorHelper(rawRequest, std::move(request)).makeInternalError();
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
shouldReplaceParams(boost::json::object const& req) const
|
||||
{
|
||||
auto const hasParams = req.contains(JS(params));
|
||||
auto const paramsIsArray = hasParams and req.at(JS(params)).is_array();
|
||||
auto const paramsIsEmptyString =
|
||||
hasParams and req.at(JS(params)).is_string() and req.at(JS(params)).as_string().empty();
|
||||
auto const paramsIsEmptyObject =
|
||||
hasParams and req.at(JS(params)).is_object() and req.at(JS(params)).as_object().empty();
|
||||
auto const paramsIsNull = hasParams and req.at(JS(params)).is_null();
|
||||
auto const arrayIsEmpty = paramsIsArray and req.at(JS(params)).as_array().empty();
|
||||
auto const arrayIsNotEmpty = paramsIsArray and not req.at(JS(params)).as_array().empty();
|
||||
auto const firstArgIsNull = arrayIsNotEmpty and req.at(JS(params)).as_array().at(0).is_null();
|
||||
auto const firstArgIsEmptyString = arrayIsNotEmpty and req.at(JS(params)).as_array().at(0).is_string() and
|
||||
req.at(JS(params)).as_array().at(0).as_string().empty();
|
||||
|
||||
// Note: all this compatibility dance is to match `rippled` as close as possible
|
||||
return not hasParams or paramsIsEmptyString or paramsIsNull or paramsIsEmptyObject or arrayIsEmpty or
|
||||
firstArgIsEmptyString or firstArgIsNull;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace web::ng
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#include "web/ng/Request.hpp"
|
||||
|
||||
#include "util/OverloadSet.hpp"
|
||||
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
@@ -104,6 +106,18 @@ Request::target() const
|
||||
return httpRequest().target();
|
||||
}
|
||||
|
||||
Request::HttpHeaders const&
|
||||
Request::httpHeaders() const
|
||||
{
|
||||
return std::visit(
|
||||
util::OverloadSet{
|
||||
[](HttpRequest const& httpRequest) -> HttpHeaders const& { return httpRequest; },
|
||||
[](WsData const& wsData) -> HttpHeaders const& { return wsData.headers.get(); }
|
||||
},
|
||||
data_
|
||||
);
|
||||
}
|
||||
|
||||
std::optional<std::string_view>
|
||||
Request::headerValue(boost::beast::http::field headerName) const
|
||||
{
|
||||
|
||||
@@ -112,6 +112,14 @@ public:
|
||||
std::optional<std::string_view>
|
||||
target() const;
|
||||
|
||||
/**
|
||||
* @brief Get the headers of the request.
|
||||
*
|
||||
* @return The headers of the request.
|
||||
*/
|
||||
HttpHeaders const&
|
||||
httpHeaders() const;
|
||||
|
||||
/**
|
||||
* @brief Get the value of a header.
|
||||
*
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "web/ng/Response.hpp"
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/OverloadSet.hpp"
|
||||
#include "util/build/Build.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
|
||||
@@ -34,83 +35,98 @@
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string_view
|
||||
asString(Response::HttpData::ContentType type)
|
||||
template <typename T>
|
||||
consteval bool
|
||||
isString()
|
||||
{
|
||||
switch (type) {
|
||||
case Response::HttpData::ContentType::TextHtml:
|
||||
return "text/html";
|
||||
case Response::HttpData::ContentType::ApplicationJson:
|
||||
return "application/json";
|
||||
}
|
||||
ASSERT(false, "Unknown content type");
|
||||
std::unreachable();
|
||||
return std::is_same_v<T, std::string>;
|
||||
}
|
||||
|
||||
http::response<http::string_body>
|
||||
prepareResponse(http::response<http::string_body> response, http::request<http::string_body> const& request)
|
||||
{
|
||||
response.set(http::field::server, fmt::format("clio-server-{}", util::build::getClioVersionString()));
|
||||
response.keep_alive(request.keep_alive());
|
||||
response.prepare_payload();
|
||||
return response;
|
||||
}
|
||||
|
||||
template <typename MessageType>
|
||||
std::optional<Response::HttpData>
|
||||
makeHttpData(http::status status, Request const& request)
|
||||
std::variant<http::response<http::string_body>, std::string>
|
||||
makeData(http::status status, MessageType message, Request const& request)
|
||||
{
|
||||
if (request.isHttp()) {
|
||||
auto const& httpRequest = request.asHttpRequest()->get();
|
||||
auto constexpr contentType = std::is_same_v<std::remove_cvref_t<MessageType>, std::string>
|
||||
? Response::HttpData::ContentType::TextHtml
|
||||
: Response::HttpData::ContentType::ApplicationJson;
|
||||
return Response::HttpData{
|
||||
.status = status,
|
||||
.contentType = contentType,
|
||||
.keepAlive = httpRequest.keep_alive(),
|
||||
.version = httpRequest.version()
|
||||
};
|
||||
std::string body;
|
||||
if constexpr (isString<MessageType>()) {
|
||||
body = std::move(message);
|
||||
} else {
|
||||
body = boost::json::serialize(message);
|
||||
}
|
||||
return std::nullopt;
|
||||
|
||||
if (not request.isHttp())
|
||||
return body;
|
||||
|
||||
auto const& httpRequest = request.asHttpRequest()->get();
|
||||
std::string const contentType = isString<MessageType>() ? "text/html" : "application/json";
|
||||
|
||||
http::response<http::string_body> result{status, httpRequest.version(), std::move(body)};
|
||||
result.set(http::field::content_type, contentType);
|
||||
return prepareResponse(std::move(result), httpRequest);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Response::Response(boost::beast::http::status status, std::string message, Request const& request)
|
||||
: message_(std::move(message)), httpData_{makeHttpData<decltype(message)>(status, request)}
|
||||
: data_{makeData(status, std::move(message), request)}
|
||||
{
|
||||
}
|
||||
|
||||
Response::Response(boost::beast::http::status status, boost::json::object const& message, Request const& request)
|
||||
: message_(boost::json::serialize(message)), httpData_{makeHttpData<decltype(message)>(status, request)}
|
||||
: data_{makeData(status, message, request)}
|
||||
{
|
||||
}
|
||||
|
||||
Response::Response(boost::beast::http::response<boost::beast::http::string_body> response, Request const& request)
|
||||
{
|
||||
ASSERT(request.isHttp(), "Request must be HTTP to construct response from HTTP response");
|
||||
data_ = prepareResponse(std::move(response), request.asHttpRequest()->get());
|
||||
}
|
||||
|
||||
std::string const&
|
||||
Response::message() const
|
||||
{
|
||||
return message_;
|
||||
return std::visit(
|
||||
util::OverloadSet{
|
||||
[](http::response<http::string_body> const& response) -> std::string const& { return response.body(); },
|
||||
[](std::string const& message) -> std::string const& { return message; },
|
||||
},
|
||||
data_
|
||||
);
|
||||
}
|
||||
|
||||
http::response<http::string_body>
|
||||
Response::intoHttpResponse() &&
|
||||
{
|
||||
ASSERT(httpData_.has_value(), "Response must have http data to be converted into http response");
|
||||
ASSERT(std::holds_alternative<http::response<http::string_body>>(data_), "Response must contain HTTP data");
|
||||
|
||||
http::response<http::string_body> result{httpData_->status, httpData_->version};
|
||||
result.set(http::field::server, fmt::format("clio-server-{}", util::build::getClioVersionString()));
|
||||
result.set(http::field::content_type, asString(httpData_->contentType));
|
||||
result.keep_alive(httpData_->keepAlive);
|
||||
result.body() = std::move(message_);
|
||||
result.prepare_payload();
|
||||
return result;
|
||||
return std::move(std::get<http::response<http::string_body>>(data_));
|
||||
}
|
||||
|
||||
boost::asio::const_buffer
|
||||
Response::asConstBuffer() const&
|
||||
Response::asWsResponse() const&
|
||||
{
|
||||
ASSERT(not httpData_.has_value(), "Losing existing http data");
|
||||
return boost::asio::buffer(message_.data(), message_.size());
|
||||
ASSERT(std::holds_alternative<std::string>(data_), "Response must contain WebSocket data");
|
||||
auto const& message = std::get<std::string>(data_);
|
||||
return boost::asio::buffer(message.data(), message.size());
|
||||
}
|
||||
|
||||
} // namespace web::ng
|
||||
|
||||
@@ -27,8 +27,9 @@
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
@@ -36,30 +37,13 @@ namespace web::ng {
|
||||
*/
|
||||
class Response {
|
||||
public:
|
||||
/**
|
||||
* @brief The data for an HTTP response.
|
||||
*/
|
||||
struct HttpData {
|
||||
/**
|
||||
* @brief The content type of the response.
|
||||
*/
|
||||
enum class ContentType { ApplicationJson, TextHtml };
|
||||
|
||||
boost::beast::http::status status; ///< The HTTP status.
|
||||
ContentType contentType; ///< The content type.
|
||||
bool keepAlive; ///< Whether the connection should be kept alive.
|
||||
unsigned int version; ///< The HTTP version.
|
||||
};
|
||||
|
||||
private:
|
||||
std::string message_;
|
||||
std::optional<HttpData> httpData_;
|
||||
std::variant<boost::beast::http::response<boost::beast::http::string_body>, std::string> data_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a Response from string. Content type will be text/html.
|
||||
*
|
||||
* @param status The HTTP status.
|
||||
* @param status The HTTP status. It will be ignored if request is WebSocket.
|
||||
* @param message The message to send.
|
||||
* @param request The request that triggered this response. Used to determine whether the response should contain
|
||||
* HTTP or WebSocket data.
|
||||
@@ -69,13 +53,21 @@ public:
|
||||
/**
|
||||
* @brief Construct a Response from JSON object. Content type will be application/json.
|
||||
*
|
||||
* @param status The HTTP status.
|
||||
* @param status The HTTP status. It will be ignored if request is WebSocket.
|
||||
* @param message The message to send.
|
||||
* @param request The request that triggered this response. Used to determine whether the response should contain
|
||||
* HTTP or WebSocket
|
||||
*/
|
||||
Response(boost::beast::http::status status, boost::json::object const& message, Request const& request);
|
||||
|
||||
/**
|
||||
* @brief Construct a Response from HTTP response.
|
||||
*
|
||||
* @param response The HTTP response.
|
||||
* @param request The request that triggered this response. It must be an HTTP request.
|
||||
*/
|
||||
Response(boost::beast::http::response<boost::beast::http::string_body> response, Request const& request);
|
||||
|
||||
/**
|
||||
* @brief Get the message of the response.
|
||||
*
|
||||
@@ -100,7 +92,7 @@ public:
|
||||
* @return The message of the response as a const buffer.
|
||||
*/
|
||||
boost::asio::const_buffer
|
||||
asConstBuffer() const&;
|
||||
asWsResponse() const&;
|
||||
};
|
||||
|
||||
} // namespace web::ng
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/ProcessingPolicy.hpp"
|
||||
#include "web/ng/impl/HttpConnection.hpp"
|
||||
#include "web/ng/impl/ServerSslContext.hpp"
|
||||
|
||||
@@ -41,6 +42,7 @@
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
#include <fmt/compile.h>
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -178,14 +180,16 @@ Server::Server(
|
||||
boost::asio::io_context& ctx,
|
||||
boost::asio::ip::tcp::endpoint endpoint,
|
||||
std::optional<boost::asio::ssl::context> sslContext,
|
||||
impl::ConnectionHandler connectionHandler,
|
||||
util::TagDecoratorFactory tagDecoratorFactory
|
||||
ProcessingPolicy processingPolicy,
|
||||
std::optional<size_t> parallelRequestLimit,
|
||||
util::TagDecoratorFactory tagDecoratorFactory,
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize
|
||||
)
|
||||
: ctx_{ctx}
|
||||
, sslContext_{std::move(sslContext)}
|
||||
, connectionHandler_{std::move(connectionHandler)}
|
||||
, endpoint_{std::move(endpoint)}
|
||||
, tagDecoratorFactory_{tagDecoratorFactory}
|
||||
, connectionHandler_{processingPolicy, parallelRequestLimit, tagDecoratorFactory_, maxSubscriptionSendQueueSize}
|
||||
, endpoint_{std::move(endpoint)}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -297,24 +301,28 @@ make_Server(util::Config const& config, boost::asio::io_context& context)
|
||||
if (not expectedSslContext)
|
||||
return std::unexpected{std::move(expectedSslContext).error()};
|
||||
|
||||
impl::ConnectionHandler::ProcessingPolicy processingPolicy{impl::ConnectionHandler::ProcessingPolicy::Parallel};
|
||||
ProcessingPolicy processingPolicy{ProcessingPolicy::Parallel};
|
||||
std::optional<size_t> parallelRequestLimit;
|
||||
|
||||
auto const processingStrategyStr = serverConfig.valueOr<std::string>("processing_policy", "parallel");
|
||||
if (processingStrategyStr == "sequent") {
|
||||
processingPolicy = impl::ConnectionHandler::ProcessingPolicy::Sequential;
|
||||
processingPolicy = ProcessingPolicy::Sequential;
|
||||
} else if (processingStrategyStr == "parallel") {
|
||||
parallelRequestLimit = serverConfig.maybeValue<size_t>("parallel_requests_limit");
|
||||
} else {
|
||||
return std::unexpected{fmt::format("Invalid 'server.processing_strategy': {}", processingStrategyStr)};
|
||||
}
|
||||
|
||||
auto const maxSubscriptionSendQueueSize = serverConfig.maybeValue<size_t>("ws_max_sending_queue_size");
|
||||
|
||||
return Server{
|
||||
context,
|
||||
std::move(endpoint).value(),
|
||||
std::move(expectedSslContext).value(),
|
||||
impl::ConnectionHandler{processingPolicy, parallelRequestLimit},
|
||||
util::TagDecoratorFactory(config)
|
||||
processingPolicy,
|
||||
parallelRequestLimit,
|
||||
util::TagDecoratorFactory(config),
|
||||
maxSubscriptionSendQueueSize
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/impl/AdminVerificationStrategy.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/ProcessingPolicy.hpp"
|
||||
#include "web/ng/impl/ConnectionHandler.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
@@ -44,16 +44,15 @@ namespace web::ng {
|
||||
class Server {
|
||||
util::Logger log_{"WebServer"};
|
||||
util::Logger perfLog_{"Performance"};
|
||||
std::reference_wrapper<boost::asio::io_context> ctx_;
|
||||
|
||||
std::reference_wrapper<boost::asio::io_context> ctx_;
|
||||
std::optional<boost::asio::ssl::context> sslContext_;
|
||||
|
||||
impl::ConnectionHandler connectionHandler_;
|
||||
|
||||
boost::asio::ip::tcp::endpoint endpoint_;
|
||||
|
||||
util::TagDecoratorFactory tagDecoratorFactory_;
|
||||
|
||||
impl::ConnectionHandler connectionHandler_;
|
||||
boost::asio::ip::tcp::endpoint endpoint_;
|
||||
|
||||
bool running_{false};
|
||||
|
||||
public:
|
||||
@@ -63,15 +62,20 @@ public:
|
||||
* @param ctx The boost::asio::io_context to use.
|
||||
* @param endpoint The endpoint to listen on.
|
||||
* @param sslContext The SSL context to use (optional).
|
||||
* @param connectionHandler The connection handler.
|
||||
* @param processingPolicy The requests processing policy (parallel or sequential).
|
||||
* @param parallelRequestLimit The limit of requests for one connection that can be processed in parallel. Only used
|
||||
* if processingPolicy is parallel.
|
||||
* @param tagDecoratorFactory The tag decorator factory.
|
||||
* @param maxSubscriptionSendQueueSize The maximum size of the subscription send queue.
|
||||
*/
|
||||
Server(
|
||||
boost::asio::io_context& ctx,
|
||||
boost::asio::ip::tcp::endpoint endpoint,
|
||||
std::optional<boost::asio::ssl::context> sslContext,
|
||||
impl::ConnectionHandler connectionHandler,
|
||||
util::TagDecoratorFactory tagDecoratorFactory
|
||||
ProcessingPolicy processingPolicy,
|
||||
std::optional<size_t> parallelRequestLimit,
|
||||
util::TagDecoratorFactory tagDecoratorFactory,
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
101
src/web/ng/SubscriptionContext.cpp
Normal file
101
src/web/ng/SubscriptionContext.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 "web/ng/SubscriptionContext.hpp"
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <boost/asio/any_io_executor.hpp>
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
SubscriptionContext::SubscriptionContext(
|
||||
util::TagDecoratorFactory const& factory,
|
||||
impl::WsConnectionBase& connection,
|
||||
std::optional<size_t> maxSendQueueSize,
|
||||
boost::asio::yield_context yield,
|
||||
ErrorHandler errorHandler
|
||||
)
|
||||
: web::SubscriptionContextInterface(factory)
|
||||
, connection_(connection)
|
||||
, maxSendQueueSize_(maxSendQueueSize)
|
||||
, tasksGroup_(yield)
|
||||
, yield_(yield)
|
||||
, errorHandler_(std::move(errorHandler))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionContext::send(std::shared_ptr<std::string> message)
|
||||
{
|
||||
if (disconnected_)
|
||||
return;
|
||||
|
||||
if (maxSendQueueSize_.has_value() and tasksGroup_.size() >= *maxSendQueueSize_) {
|
||||
tasksGroup_.spawn(yield_, [this](boost::asio::yield_context innerYield) {
|
||||
connection_.get().close(innerYield);
|
||||
});
|
||||
disconnected_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
tasksGroup_.spawn(yield_, [this, message = std::move(message)](boost::asio::yield_context innerYield) {
|
||||
auto const maybeError = connection_.get().sendBuffer(boost::asio::buffer(*message), innerYield);
|
||||
if (maybeError.has_value() and errorHandler_(*maybeError, connection_))
|
||||
connection_.get().close(innerYield);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionContext::onDisconnect(OnDisconnectSlot const& slot)
|
||||
{
|
||||
onDisconnect_.connect(slot);
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionContext::setApiSubversion(uint32_t value)
|
||||
{
|
||||
apiSubversion_ = value;
|
||||
}
|
||||
|
||||
uint32_t
|
||||
SubscriptionContext::apiSubversion() const
|
||||
{
|
||||
return apiSubversion_;
|
||||
}
|
||||
|
||||
void
|
||||
SubscriptionContext::disconnect(boost::asio::yield_context yield)
|
||||
{
|
||||
onDisconnect_(this);
|
||||
disconnected_ = true;
|
||||
tasksGroup_.asyncWait(yield);
|
||||
}
|
||||
|
||||
} // namespace web::ng
|
||||
132
src/web/ng/SubscriptionContext.hpp
Normal file
132
src/web/ng/SubscriptionContext.hpp
Normal file
@@ -0,0 +1,132 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/CoroutineGroup.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/impl/WsConnection.hpp"
|
||||
|
||||
#include <boost/asio/any_io_executor.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/signals2/variadic_signal.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace web::ng {
|
||||
|
||||
/**
|
||||
* @brief Implementation of SubscriptionContextInterface.
|
||||
* @note This class is designed to be used with SubscriptionManager. The class is safe to use from multiple threads.
|
||||
* The method disconnect() must be called before the object is destroyed.
|
||||
*/
|
||||
class SubscriptionContext : public web::SubscriptionContextInterface {
|
||||
public:
|
||||
/**
|
||||
* @brief Error handler definition. Error handler returns true if connection should be closed false otherwise.
|
||||
*/
|
||||
using ErrorHandler = std::function<bool(Error const&, Connection const&)>;
|
||||
|
||||
private:
|
||||
std::reference_wrapper<impl::WsConnectionBase> connection_;
|
||||
std::optional<size_t> maxSendQueueSize_;
|
||||
util::CoroutineGroup tasksGroup_;
|
||||
boost::asio::yield_context yield_;
|
||||
ErrorHandler errorHandler_;
|
||||
|
||||
boost::signals2::signal<void(SubscriptionContextInterface*)> onDisconnect_;
|
||||
std::atomic_bool disconnected_{false};
|
||||
|
||||
/**
|
||||
* @brief The API version of the web stream client.
|
||||
* This is used to track the api version of this connection, which mainly is used by subscription. It is different
|
||||
* from the api version in Context, which is only used for the current request.
|
||||
*/
|
||||
std::atomic_uint32_t apiSubversion_ = 0u;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Subscription Context object
|
||||
*
|
||||
* @param factory The tag decorator factory to use to init taggable.
|
||||
* @param connection The connection for which the context is created.
|
||||
* @param maxSendQueueSize The maximum size of the send queue. If the queue is full, the connection will be closed.
|
||||
* @param yield The yield context to spawn sending coroutines.
|
||||
* @param errorHandler The error handler.
|
||||
*/
|
||||
SubscriptionContext(
|
||||
util::TagDecoratorFactory const& factory,
|
||||
impl::WsConnectionBase& connection,
|
||||
std::optional<size_t> maxSendQueueSize,
|
||||
boost::asio::yield_context yield,
|
||||
ErrorHandler errorHandler
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Send message to the client
|
||||
* @note This method does nothing after disconnected() was called.
|
||||
*
|
||||
* @param message The message to send.
|
||||
*/
|
||||
void
|
||||
send(std::shared_ptr<std::string> message) override;
|
||||
|
||||
/**
|
||||
* @brief Connect a slot to onDisconnect connection signal.
|
||||
*
|
||||
* @param slot The slot to connect.
|
||||
*/
|
||||
void
|
||||
onDisconnect(OnDisconnectSlot const& slot) override;
|
||||
|
||||
/**
|
||||
* @brief Set the API subversion.
|
||||
* @param value The value to set.
|
||||
*/
|
||||
void
|
||||
setApiSubversion(uint32_t value) override;
|
||||
|
||||
/**
|
||||
* @brief Get the API subversion.
|
||||
*
|
||||
* @return The API subversion.
|
||||
*/
|
||||
uint32_t
|
||||
apiSubversion() const override;
|
||||
|
||||
/**
|
||||
* @brief Notify the context that related connection is disconnected and wait for all the task to complete.
|
||||
* @note This method must be called before the object is destroyed.
|
||||
*
|
||||
* @param yield The yield context to wait for all the tasks to complete.
|
||||
*/
|
||||
void
|
||||
disconnect(boost::asio::yield_context yield);
|
||||
};
|
||||
|
||||
} // namespace web::ng
|
||||
@@ -21,21 +21,30 @@
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/CoroutineGroup.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/ProcessingPolicy.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/SubscriptionContext.hpp"
|
||||
|
||||
#include <boost/asio/bind_cancellation_slot.hpp>
|
||||
#include <boost/asio/cancellation_signal.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/error.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/beast/http/error.hpp>
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/beast/websocket/error.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -47,7 +56,8 @@ namespace {
|
||||
|
||||
Response
|
||||
handleHttpRequest(
|
||||
ConnectionContext const& connectionContext,
|
||||
ConnectionMetadata& connectionMetadata,
|
||||
SubscriptionContextPtr& subscriptionContext,
|
||||
ConnectionHandler::TargetToHandlerMap const& handlers,
|
||||
Request const& request,
|
||||
boost::asio::yield_context yield
|
||||
@@ -58,12 +68,13 @@ handleHttpRequest(
|
||||
if (it == handlers.end()) {
|
||||
return Response{boost::beast::http::status::bad_request, "Bad target", request};
|
||||
}
|
||||
return it->second(request, connectionContext, yield);
|
||||
return it->second(request, connectionMetadata, subscriptionContext, yield);
|
||||
}
|
||||
|
||||
Response
|
||||
handleWsRequest(
|
||||
ConnectionContext connectionContext,
|
||||
ConnectionMetadata& connectionMetadata,
|
||||
SubscriptionContextPtr& subscriptionContext,
|
||||
std::optional<MessageHandler> const& handler,
|
||||
Request const& request,
|
||||
boost::asio::yield_context yield
|
||||
@@ -72,7 +83,7 @@ handleWsRequest(
|
||||
if (not handler.has_value()) {
|
||||
return Response{boost::beast::http::status::bad_request, "WebSocket is not supported by this server", request};
|
||||
}
|
||||
return handler->operator()(request, connectionContext, yield);
|
||||
return handler->operator()(request, connectionMetadata, subscriptionContext, yield);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -95,8 +106,16 @@ ConnectionHandler::StringHash::operator()(std::string const& str) const
|
||||
return hash_type{}(str);
|
||||
}
|
||||
|
||||
ConnectionHandler::ConnectionHandler(ProcessingPolicy processingPolicy, std::optional<size_t> maxParallelRequests)
|
||||
: processingPolicy_{processingPolicy}, maxParallelRequests_{maxParallelRequests}
|
||||
ConnectionHandler::ConnectionHandler(
|
||||
ProcessingPolicy processingPolicy,
|
||||
std::optional<size_t> maxParallelRequests,
|
||||
util::TagDecoratorFactory& tagFactory,
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize
|
||||
)
|
||||
: processingPolicy_{processingPolicy}
|
||||
, maxParallelRequests_{maxParallelRequests}
|
||||
, tagFactory_{tagFactory}
|
||||
, maxSubscriptionSendQueueSize_{maxSubscriptionSendQueueSize}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -126,14 +145,32 @@ ConnectionHandler::processConnection(ConnectionPtr connectionPtr, boost::asio::y
|
||||
|
||||
bool shouldCloseGracefully = false;
|
||||
|
||||
std::shared_ptr<SubscriptionContext> subscriptionContext;
|
||||
if (connectionRef.wasUpgraded()) {
|
||||
auto* ptr = dynamic_cast<impl::WsConnectionBase*>(connectionPtr.get());
|
||||
ASSERT(ptr != nullptr, "Casted not websocket connection");
|
||||
subscriptionContext = std::make_shared<SubscriptionContext>(
|
||||
tagFactory_,
|
||||
*ptr,
|
||||
maxSubscriptionSendQueueSize_,
|
||||
yield,
|
||||
[this](Error const& e, Connection const& c) { return handleError(e, c); }
|
||||
);
|
||||
}
|
||||
SubscriptionContextPtr subscriptionContextInterfacePtr = subscriptionContext;
|
||||
|
||||
switch (processingPolicy_) {
|
||||
case ProcessingPolicy::Sequential:
|
||||
shouldCloseGracefully = sequentRequestResponseLoop(connectionRef, yield);
|
||||
shouldCloseGracefully = sequentRequestResponseLoop(connectionRef, subscriptionContextInterfacePtr, yield);
|
||||
break;
|
||||
case ProcessingPolicy::Parallel:
|
||||
shouldCloseGracefully = parallelRequestResponseLoop(connectionRef, yield);
|
||||
shouldCloseGracefully = parallelRequestResponseLoop(connectionRef, subscriptionContextInterfacePtr, yield);
|
||||
break;
|
||||
}
|
||||
|
||||
if (subscriptionContext != nullptr)
|
||||
subscriptionContext->disconnect(yield);
|
||||
|
||||
if (shouldCloseGracefully)
|
||||
connectionRef.close(yield);
|
||||
|
||||
@@ -179,7 +216,11 @@ ConnectionHandler::handleError(Error const& error, Connection const& connection)
|
||||
}
|
||||
|
||||
bool
|
||||
ConnectionHandler::sequentRequestResponseLoop(Connection& connection, boost::asio::yield_context yield)
|
||||
ConnectionHandler::sequentRequestResponseLoop(
|
||||
Connection& connection,
|
||||
SubscriptionContextPtr& subscriptionContext,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
// The loop here is infinite because:
|
||||
// - For websocket connection is persistent so Clio will try to read and respond infinite unless client
|
||||
@@ -196,14 +237,19 @@ ConnectionHandler::sequentRequestResponseLoop(Connection& connection, boost::asi
|
||||
|
||||
LOG(log_.info()) << connection.tag() << "Received request from ip = " << connection.ip();
|
||||
|
||||
auto maybeReturnValue = processRequest(connection, std::move(expectedRequest).value(), yield);
|
||||
auto maybeReturnValue =
|
||||
processRequest(connection, subscriptionContext, std::move(expectedRequest).value(), yield);
|
||||
if (maybeReturnValue.has_value())
|
||||
return maybeReturnValue.value();
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ConnectionHandler::parallelRequestResponseLoop(Connection& connection, boost::asio::yield_context yield)
|
||||
ConnectionHandler::parallelRequestResponseLoop(
|
||||
Connection& connection,
|
||||
SubscriptionContextPtr& subscriptionContext,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
// atomic_bool is not needed here because everything happening on coroutine's strand
|
||||
bool stop = false;
|
||||
@@ -218,13 +264,18 @@ ConnectionHandler::parallelRequestResponseLoop(Connection& connection, boost::as
|
||||
closeConnectionGracefully &= closeGracefully;
|
||||
break;
|
||||
}
|
||||
if (tasksGroup.canSpawn()) {
|
||||
|
||||
if (not tasksGroup.isFull()) {
|
||||
bool const spawnSuccess = tasksGroup.spawn(
|
||||
yield, // spawn on the same strand
|
||||
[this, &stop, &closeConnectionGracefully, &connection, request = std::move(expectedRequest).value()](
|
||||
boost::asio::yield_context innerYield
|
||||
) mutable {
|
||||
auto maybeCloseConnectionGracefully = processRequest(connection, request, innerYield);
|
||||
[this,
|
||||
&stop,
|
||||
&closeConnectionGracefully,
|
||||
&connection,
|
||||
&subscriptionContext,
|
||||
request = std::move(expectedRequest).value()](boost::asio::yield_context innerYield) mutable {
|
||||
auto maybeCloseConnectionGracefully =
|
||||
processRequest(connection, subscriptionContext, request, innerYield);
|
||||
if (maybeCloseConnectionGracefully.has_value()) {
|
||||
stop = true;
|
||||
closeConnectionGracefully &= maybeCloseConnectionGracefully.value();
|
||||
@@ -248,9 +299,14 @@ ConnectionHandler::parallelRequestResponseLoop(Connection& connection, boost::as
|
||||
}
|
||||
|
||||
std::optional<bool>
|
||||
ConnectionHandler::processRequest(Connection& connection, Request const& request, boost::asio::yield_context yield)
|
||||
ConnectionHandler::processRequest(
|
||||
Connection& connection,
|
||||
SubscriptionContextPtr& subscriptionContext,
|
||||
Request const& request,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
auto response = handleRequest(connection.context(), request, yield);
|
||||
auto response = handleRequest(connection, subscriptionContext, request, yield);
|
||||
|
||||
auto const maybeError = connection.send(std::move(response), yield);
|
||||
if (maybeError.has_value()) {
|
||||
@@ -261,18 +317,19 @@ ConnectionHandler::processRequest(Connection& connection, Request const& request
|
||||
|
||||
Response
|
||||
ConnectionHandler::handleRequest(
|
||||
ConnectionContext const& connectionContext,
|
||||
ConnectionMetadata& connectionMetadata,
|
||||
SubscriptionContextPtr& subscriptionContext,
|
||||
Request const& request,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
switch (request.method()) {
|
||||
case Request::Method::Get:
|
||||
return handleHttpRequest(connectionContext, getHandlers_, request, yield);
|
||||
return handleHttpRequest(connectionMetadata, subscriptionContext, getHandlers_, request, yield);
|
||||
case Request::Method::Post:
|
||||
return handleHttpRequest(connectionContext, postHandlers_, request, yield);
|
||||
return handleHttpRequest(connectionMetadata, subscriptionContext, postHandlers_, request, yield);
|
||||
case Request::Method::Websocket:
|
||||
return handleWsRequest(connectionContext, wsHandler_, request, yield);
|
||||
return handleWsRequest(connectionMetadata, subscriptionContext, wsHandler_, request, yield);
|
||||
default:
|
||||
return Response{boost::beast::http::status::bad_request, "Unsupported http method", request};
|
||||
}
|
||||
|
||||
@@ -19,10 +19,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/log/Logger.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/MessageHandler.hpp"
|
||||
#include "web/ng/ProcessingPolicy.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
|
||||
@@ -41,8 +44,6 @@ namespace web::ng::impl {
|
||||
|
||||
class ConnectionHandler {
|
||||
public:
|
||||
enum class ProcessingPolicy { Sequential, Parallel };
|
||||
|
||||
struct StringHash {
|
||||
using hash_type = std::hash<std::string_view>;
|
||||
using is_transparent = void;
|
||||
@@ -64,6 +65,9 @@ private:
|
||||
ProcessingPolicy processingPolicy_;
|
||||
std::optional<size_t> maxParallelRequests_;
|
||||
|
||||
std::reference_wrapper<util::TagDecoratorFactory> tagFactory_;
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize_;
|
||||
|
||||
TargetToHandlerMap getHandlers_;
|
||||
TargetToHandlerMap postHandlers_;
|
||||
std::optional<MessageHandler> wsHandler_;
|
||||
@@ -71,7 +75,12 @@ private:
|
||||
boost::signals2::signal<void()> onStop_;
|
||||
|
||||
public:
|
||||
ConnectionHandler(ProcessingPolicy processingPolicy, std::optional<size_t> maxParallelRequests);
|
||||
ConnectionHandler(
|
||||
ProcessingPolicy processingPolicy,
|
||||
std::optional<size_t> maxParallelRequests,
|
||||
util::TagDecoratorFactory& tagFactory,
|
||||
std::optional<size_t> maxSubscriptionSendQueueSize
|
||||
);
|
||||
|
||||
void
|
||||
onGet(std::string const& target, MessageHandler handler);
|
||||
@@ -107,24 +116,34 @@ private:
|
||||
* @return True if the connection should be gracefully closed, false otherwise.
|
||||
*/
|
||||
bool
|
||||
sequentRequestResponseLoop(Connection& connection, boost::asio::yield_context yield);
|
||||
sequentRequestResponseLoop(
|
||||
Connection& connection,
|
||||
SubscriptionContextPtr& subscriptionContext,
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
|
||||
bool
|
||||
parallelRequestResponseLoop(Connection& connection, boost::asio::yield_context yield);
|
||||
parallelRequestResponseLoop(
|
||||
Connection& connection,
|
||||
SubscriptionContextPtr& subscriptionContext,
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
|
||||
std::optional<bool>
|
||||
processRequest(Connection& connection, Request const& request, boost::asio::yield_context yield);
|
||||
processRequest(
|
||||
Connection& connection,
|
||||
SubscriptionContextPtr& subscriptionContext,
|
||||
Request const& request,
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Handle a request.
|
||||
*
|
||||
* @param connectionContext The connection context.
|
||||
* @param request The request to handle.
|
||||
* @param yield The yield context.
|
||||
* @return The response to send.
|
||||
*/
|
||||
Response
|
||||
handleRequest(ConnectionContext const& connectionContext, Request const& request, boost::asio::yield_context yield);
|
||||
handleRequest(
|
||||
ConnectionMetadata& connectionMetadata,
|
||||
SubscriptionContextPtr& subscriptionContext,
|
||||
Request const& request,
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
};
|
||||
|
||||
} // namespace web::ng::impl
|
||||
|
||||
165
src/web/ng/impl/ErrorHandling.cpp
Normal file
165
src/web/ng/impl/ErrorHandling.cpp
Normal file
@@ -0,0 +1,165 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 "web/ng/impl/ErrorHandling.hpp"
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/JS.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
namespace web::ng::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
boost::json::object
|
||||
composeErrorImpl(auto const& error, Request const& rawRequest, std::optional<boost::json::object> const& request)
|
||||
{
|
||||
auto e = rpc::makeError(error);
|
||||
|
||||
if (request) {
|
||||
auto const appendFieldIfExist = [&](auto const& field) {
|
||||
if (request->contains(field) and not request->at(field).is_null())
|
||||
e[field] = request->at(field);
|
||||
};
|
||||
|
||||
appendFieldIfExist(JS(id));
|
||||
|
||||
if (not rawRequest.isHttp())
|
||||
appendFieldIfExist(JS(api_version));
|
||||
|
||||
e[JS(request)] = request.value();
|
||||
}
|
||||
|
||||
if (not rawRequest.isHttp()) {
|
||||
return e;
|
||||
}
|
||||
return {{JS(result), e}};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ErrorHelper::ErrorHelper(Request const& rawRequest, std::optional<boost::json::object> request)
|
||||
: rawRequest_{rawRequest}, request_{std::move(request)}
|
||||
{
|
||||
}
|
||||
|
||||
Response
|
||||
ErrorHelper::makeError(rpc::Status const& err) const
|
||||
{
|
||||
if (not rawRequest_.get().isHttp()) {
|
||||
return Response{http::status::bad_request, composeError(err), rawRequest_};
|
||||
}
|
||||
|
||||
// Note: a collection of crutches to match rippled output follows
|
||||
if (auto const clioCode = std::get_if<rpc::ClioError>(&err.code)) {
|
||||
switch (*clioCode) {
|
||||
case rpc::ClioError::rpcINVALID_API_VERSION:
|
||||
return Response{
|
||||
http::status::bad_request, std::string{rpc::getErrorInfo(*clioCode).error}, rawRequest_
|
||||
};
|
||||
case rpc::ClioError::rpcCOMMAND_IS_MISSING:
|
||||
return Response{http::status::bad_request, "Null method", rawRequest_};
|
||||
case rpc::ClioError::rpcCOMMAND_IS_EMPTY:
|
||||
return Response{http::status::bad_request, "method is empty", rawRequest_};
|
||||
case rpc::ClioError::rpcCOMMAND_NOT_STRING:
|
||||
return Response{http::status::bad_request, "method is not string", rawRequest_};
|
||||
case rpc::ClioError::rpcPARAMS_UNPARSEABLE:
|
||||
return Response{http::status::bad_request, "params unparseable", rawRequest_};
|
||||
|
||||
// others are not applicable but we want a compilation error next time we add one
|
||||
case rpc::ClioError::rpcUNKNOWN_OPTION:
|
||||
case rpc::ClioError::rpcMALFORMED_CURRENCY:
|
||||
case rpc::ClioError::rpcMALFORMED_REQUEST:
|
||||
case rpc::ClioError::rpcMALFORMED_OWNER:
|
||||
case rpc::ClioError::rpcMALFORMED_ADDRESS:
|
||||
case rpc::ClioError::rpcINVALID_HOT_WALLET:
|
||||
case rpc::ClioError::rpcFIELD_NOT_FOUND_TRANSACTION:
|
||||
case rpc::ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID:
|
||||
case rpc::ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS:
|
||||
case rpc::ClioError::etlCONNECTION_ERROR:
|
||||
case rpc::ClioError::etlREQUEST_ERROR:
|
||||
case rpc::ClioError::etlREQUEST_TIMEOUT:
|
||||
case rpc::ClioError::etlINVALID_RESPONSE:
|
||||
ASSERT(false, "Unknown rpc error code {}", static_cast<int>(*clioCode)); // this should never happen
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Response{http::status::bad_request, composeError(err), rawRequest_};
|
||||
}
|
||||
|
||||
Response
|
||||
ErrorHelper::makeInternalError() const
|
||||
{
|
||||
return Response{http::status::internal_server_error, composeError(rpc::RippledError::rpcINTERNAL), rawRequest_};
|
||||
}
|
||||
|
||||
Response
|
||||
ErrorHelper::makeNotReadyError() const
|
||||
{
|
||||
return Response{http::status::ok, composeError(rpc::RippledError::rpcNOT_READY), rawRequest_};
|
||||
}
|
||||
|
||||
Response
|
||||
ErrorHelper::makeTooBusyError() const
|
||||
{
|
||||
if (not rawRequest_.get().isHttp()) {
|
||||
return Response{http::status::too_many_requests, rpc::makeError(rpc::RippledError::rpcTOO_BUSY), rawRequest_};
|
||||
}
|
||||
|
||||
return Response{http::status::service_unavailable, rpc::makeError(rpc::RippledError::rpcTOO_BUSY), rawRequest_};
|
||||
}
|
||||
|
||||
Response
|
||||
ErrorHelper::makeJsonParsingError() const
|
||||
{
|
||||
if (not rawRequest_.get().isHttp()) {
|
||||
return Response{http::status::bad_request, rpc::makeError(rpc::RippledError::rpcBAD_SYNTAX), rawRequest_};
|
||||
}
|
||||
|
||||
return Response{http::status::bad_request, fmt::format("Unable to parse JSON from the request"), rawRequest_};
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
ErrorHelper::composeError(rpc::Status const& error) const
|
||||
{
|
||||
return composeErrorImpl(error, rawRequest_, request_);
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
ErrorHelper::composeError(rpc::RippledError error) const
|
||||
{
|
||||
return composeErrorImpl(error, rawRequest_, request_);
|
||||
}
|
||||
|
||||
} // namespace web::ng::impl
|
||||
114
src/web/ng/impl/ErrorHandling.hpp
Normal file
114
src/web/ng/impl/ErrorHandling.hpp
Normal file
@@ -0,0 +1,114 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
namespace web::ng::impl {
|
||||
|
||||
/**
|
||||
* @brief A helper that attempts to match rippled reporting mode HTTP errors as close as possible.
|
||||
*/
|
||||
class ErrorHelper {
|
||||
std::reference_wrapper<Request const> rawRequest_;
|
||||
std::optional<boost::json::object> request_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Error Helper object
|
||||
*
|
||||
* @param rawRequest The request that caused the error.
|
||||
* @param request The parsed request that caused the error.
|
||||
*/
|
||||
ErrorHelper(Request const& rawRequest, std::optional<boost::json::object> request = std::nullopt);
|
||||
|
||||
/**
|
||||
* @brief Make an error response from a status.
|
||||
*
|
||||
* @param err The status to make an error response from.
|
||||
* @return
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeError(rpc::Status const& err) const;
|
||||
|
||||
/**
|
||||
* @brief Make an internal error response.
|
||||
*
|
||||
* @return A response with an internal error.
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeInternalError() const;
|
||||
|
||||
/**
|
||||
* @brief Make a response for when the server is not ready.
|
||||
*
|
||||
* @return A response with a not ready error.
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeNotReadyError() const;
|
||||
|
||||
/**
|
||||
* @brief Make a response for when the server is too busy.
|
||||
*
|
||||
* @return A response with a too busy error.
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeTooBusyError() const;
|
||||
|
||||
/**
|
||||
* @brief Make a response when json parsing fails.
|
||||
*
|
||||
* @return A response with a json parsing error.
|
||||
*/
|
||||
[[nodiscard]] Response
|
||||
makeJsonParsingError() const;
|
||||
|
||||
/**
|
||||
* @beirf Compose an error into json object from a status.
|
||||
*
|
||||
* @param error The status to compose into a json object.
|
||||
* @return The composed json object.
|
||||
*/
|
||||
[[nodiscard]] boost::json::object
|
||||
composeError(rpc::Status const& error) const;
|
||||
|
||||
/**
|
||||
* @brief Compose an error into json object from a rippled error.
|
||||
*
|
||||
* @param error The rippled error to compose into a json object.
|
||||
* @return The composed json object.
|
||||
*/
|
||||
[[nodiscard]] boost::json::object
|
||||
composeError(rpc::RippledError error) const;
|
||||
};
|
||||
|
||||
} // namespace web::ng::impl
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/Concepts.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
@@ -52,8 +53,20 @@
|
||||
|
||||
namespace web::ng::impl {
|
||||
|
||||
class WsConnectionBase : public Connection {
|
||||
public:
|
||||
using Connection::Connection;
|
||||
|
||||
virtual std::optional<Error>
|
||||
sendBuffer(
|
||||
boost::asio::const_buffer buffer,
|
||||
boost::asio::yield_context yield,
|
||||
std::chrono::steady_clock::duration timeout = Connection::DEFAULT_TIMEOUT
|
||||
) = 0;
|
||||
};
|
||||
|
||||
template <typename StreamType>
|
||||
class WsConnection : public Connection {
|
||||
class WsConnection : public WsConnectionBase {
|
||||
boost::beast::websocket::stream<StreamType> stream_;
|
||||
boost::beast::http::request<boost::beast::http::string_body> initialRequest_;
|
||||
|
||||
@@ -66,7 +79,7 @@ public:
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory
|
||||
)
|
||||
requires IsTcpStream<StreamType>
|
||||
: Connection(std::move(ip), std::move(buffer), tagDecoratorFactory)
|
||||
: WsConnectionBase(std::move(ip), std::move(buffer), tagDecoratorFactory)
|
||||
, stream_(std::move(socket))
|
||||
, initialRequest_(std::move(initialRequest))
|
||||
{
|
||||
@@ -81,7 +94,7 @@ public:
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory
|
||||
)
|
||||
requires IsSslTcpStream<StreamType>
|
||||
: Connection(std::move(ip), std::move(buffer), tagDecoratorFactory)
|
||||
: WsConnectionBase(std::move(ip), std::move(buffer), tagDecoratorFactory)
|
||||
, stream_(std::move(socket), sslContext)
|
||||
, initialRequest_(std::move(initialRequest))
|
||||
{
|
||||
@@ -111,6 +124,20 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<Error>
|
||||
sendBuffer(
|
||||
boost::asio::const_buffer buffer,
|
||||
boost::asio::yield_context yield,
|
||||
std::chrono::steady_clock::duration timeout = Connection::DEFAULT_TIMEOUT
|
||||
) override
|
||||
{
|
||||
auto error =
|
||||
util::withTimeout([this, buffer](auto&& yield) { stream_.async_write(buffer, yield); }, yield, timeout);
|
||||
if (error)
|
||||
return error;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Error>
|
||||
send(
|
||||
Response response,
|
||||
@@ -118,12 +145,7 @@ public:
|
||||
std::chrono::steady_clock::duration timeout = DEFAULT_TIMEOUT
|
||||
) override
|
||||
{
|
||||
auto error = util::withTimeout(
|
||||
[this, &response](auto&& yield) { stream_.async_write(response.asConstBuffer(), yield); }, yield, timeout
|
||||
);
|
||||
if (error)
|
||||
return error;
|
||||
return std::nullopt;
|
||||
return sendBuffer(response.asWsResponse(), yield, timeout);
|
||||
}
|
||||
|
||||
std::expected<Request, Error>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#include "util/MockPrometheus.hpp"
|
||||
#include "util/MockWsBase.hpp"
|
||||
#include "util/SyncExecutionCtxFixture.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
@@ -37,25 +37,9 @@
|
||||
template <typename TestedFeed>
|
||||
struct FeedBaseTest : util::prometheus::WithPrometheus, MockBackendTest, SyncExecutionCtxFixture {
|
||||
protected:
|
||||
std::shared_ptr<web::ConnectionBase> sessionPtr;
|
||||
std::shared_ptr<TestedFeed> testFeedPtr;
|
||||
MockSession* mockSessionPtr = nullptr;
|
||||
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
testFeedPtr = std::make_shared<TestedFeed>(ctx);
|
||||
sessionPtr = std::make_shared<MockSession>();
|
||||
sessionPtr->apiSubVersion = 1;
|
||||
mockSessionPtr = dynamic_cast<MockSession*>(sessionPtr.get());
|
||||
}
|
||||
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
sessionPtr.reset();
|
||||
testFeedPtr.reset();
|
||||
}
|
||||
web::SubscriptionContextPtr sessionPtr = std::make_shared<MockSession>();
|
||||
std::shared_ptr<TestedFeed> testFeedPtr = std::make_shared<TestedFeed>(ctx);
|
||||
MockSession* mockSessionPtr = dynamic_cast<MockSession*>(sessionPtr.get());
|
||||
};
|
||||
|
||||
namespace impl {
|
||||
|
||||
@@ -34,22 +34,7 @@ template <template <typename> typename MockType = ::testing::NiceMock>
|
||||
struct HandlerBaseTestBase : util::prometheus::WithPrometheus,
|
||||
MockBackendTestBase<MockType>,
|
||||
SyncAsioContextTest,
|
||||
MockETLServiceTestBase<MockType> {
|
||||
protected:
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
SyncAsioContextTest::SetUp();
|
||||
MockETLServiceTestBase<MockType>::SetUp();
|
||||
}
|
||||
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
MockETLServiceTestBase<MockType>::TearDown();
|
||||
SyncAsioContextTest::TearDown();
|
||||
}
|
||||
};
|
||||
MockETLServiceTestBase<MockType> {};
|
||||
|
||||
/**
|
||||
* @brief Fixture with a "nice" backend mock and an embedded boost::asio context.
|
||||
|
||||
@@ -21,20 +21,25 @@
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
struct MockSession : public web::ConnectionBase {
|
||||
struct MockSession : public web::SubscriptionContextInterface {
|
||||
MOCK_METHOD(void, send, (std::shared_ptr<std::string>), (override));
|
||||
MOCK_METHOD(void, send, (std::string&&, boost::beast::http::status), (override));
|
||||
MOCK_METHOD(void, onDisconnect, (OnDisconnectSlot const&), (override));
|
||||
MOCK_METHOD(void, setApiSubversion, (uint32_t), (override));
|
||||
MOCK_METHOD(uint32_t, apiSubversion, (), (const, override));
|
||||
|
||||
util::TagDecoratorFactory tagDecoratorFactory{util::Config{}};
|
||||
|
||||
MockSession() : web::ConnectionBase(tagDecoratorFactory, "")
|
||||
MockSession() : web::SubscriptionContextInterface(tagDecoratorFactory)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <boost/asio/ssl/stream_base.hpp>
|
||||
#include <boost/asio/ssl/verify_context.hpp>
|
||||
#include <boost/asio/ssl/verify_mode.hpp>
|
||||
#include <boost/beast/core/buffers_to_string.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/stream_traits.hpp>
|
||||
|
||||
45
tests/common/web/interface/ConnectionBaseMock.hpp
Normal file
45
tests/common/web/interface/ConnectionBaseMock.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
struct ConnectionBaseMock : web::ConnectionBase {
|
||||
using ConnectionBase::ConnectionBase;
|
||||
|
||||
MOCK_METHOD(void, send, (std::string&&, boost::beast::http::status), (override));
|
||||
MOCK_METHOD(void, send, (std::shared_ptr<std::string>), (override));
|
||||
MOCK_METHOD(
|
||||
web::SubscriptionContextPtr,
|
||||
makeSubscriptionContext,
|
||||
(util::TagDecoratorFactory const& factory),
|
||||
(override)
|
||||
);
|
||||
};
|
||||
|
||||
using ConnectionBaseStrictMockPtr = std::shared_ptr<testing::StrictMock<ConnectionBaseMock>>;
|
||||
@@ -19,18 +19,29 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/HttpConnection.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
struct MockConnectionMetadataImpl : web::ng::ConnectionMetadata {
|
||||
using web::ng::ConnectionMetadata::ConnectionMetadata;
|
||||
MOCK_METHOD(bool, wasUpgraded, (), (const, override));
|
||||
};
|
||||
|
||||
using MockConnectionMetadata = testing::NiceMock<MockConnectionMetadataImpl>;
|
||||
using StrictMockConnectionMetadata = testing::StrictMock<MockConnectionMetadataImpl>;
|
||||
|
||||
struct MockConnectionImpl : web::ng::Connection {
|
||||
using web::ng::Connection::Connection;
|
||||
|
||||
|
||||
85
tests/common/web/ng/impl/MockHttpConnection.hpp
Normal file
85
tests/common/web/ng/impl/MockHttpConnection.hpp
Normal file
@@ -0,0 +1,85 @@
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/Taggable.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/HttpConnection.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
struct MockHttpConnectionImpl : web::ng::impl::UpgradableConnection {
|
||||
using UpgradableConnection::UpgradableConnection;
|
||||
|
||||
MOCK_METHOD(bool, wasUpgraded, (), (const, override));
|
||||
|
||||
using SendReturnType = std::optional<web::ng::Error>;
|
||||
MOCK_METHOD(
|
||||
SendReturnType,
|
||||
send,
|
||||
(web::ng::Response, boost::asio::yield_context, std::chrono::steady_clock::duration),
|
||||
(override)
|
||||
);
|
||||
|
||||
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
|
||||
MOCK_METHOD(
|
||||
ReceiveReturnType,
|
||||
receive,
|
||||
(boost::asio::yield_context, std::chrono::steady_clock::duration),
|
||||
(override)
|
||||
);
|
||||
|
||||
MOCK_METHOD(void, close, (boost::asio::yield_context, std::chrono::steady_clock::duration));
|
||||
|
||||
using IsUpgradeRequestedReturnType = std::expected<bool, web::ng::Error>;
|
||||
MOCK_METHOD(
|
||||
IsUpgradeRequestedReturnType,
|
||||
isUpgradeRequested,
|
||||
(boost::asio::yield_context, std::chrono::steady_clock::duration),
|
||||
(override)
|
||||
);
|
||||
|
||||
using UpgradeReturnType = std::expected<web::ng::ConnectionPtr, web::ng::Error>;
|
||||
using OptionalSslContext = std::optional<boost::asio::ssl::context>;
|
||||
MOCK_METHOD(
|
||||
UpgradeReturnType,
|
||||
upgrade,
|
||||
(OptionalSslContext & sslContext,
|
||||
util::TagDecoratorFactory const& tagDecoratorFactory,
|
||||
boost::asio::yield_context yield),
|
||||
(override)
|
||||
);
|
||||
};
|
||||
|
||||
using MockHttpConnection = testing::NiceMock<MockHttpConnectionImpl>;
|
||||
using MockHttpConnectionPtr = std::unique_ptr<testing::NiceMock<MockHttpConnectionImpl>>;
|
||||
|
||||
using StrictMockHttpConnection = testing::StrictMock<MockHttpConnectionImpl>;
|
||||
using StrictMockHttpConnectionPtr = std::unique_ptr<testing::StrictMock<MockHttpConnectionImpl>>;
|
||||
73
tests/common/web/ng/impl/MockWsConnection.hpp
Normal file
73
tests/common/web/ng/impl/MockWsConnection.hpp
Normal file
@@ -0,0 +1,73 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/WsConnection.hpp"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
struct MockWsConnectionImpl : web::ng::impl::WsConnectionBase {
|
||||
using WsConnectionBase::WsConnectionBase;
|
||||
|
||||
MOCK_METHOD(bool, wasUpgraded, (), (const, override));
|
||||
|
||||
using SendReturnType = std::optional<web::ng::Error>;
|
||||
MOCK_METHOD(
|
||||
SendReturnType,
|
||||
send,
|
||||
(web::ng::Response, boost::asio::yield_context, std::chrono::steady_clock::duration),
|
||||
(override)
|
||||
);
|
||||
|
||||
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
|
||||
MOCK_METHOD(
|
||||
ReceiveReturnType,
|
||||
receive,
|
||||
(boost::asio::yield_context, std::chrono::steady_clock::duration),
|
||||
(override)
|
||||
);
|
||||
|
||||
MOCK_METHOD(void, close, (boost::asio::yield_context, std::chrono::steady_clock::duration));
|
||||
|
||||
using SendBufferReturnType = std::optional<web::ng::Error>;
|
||||
MOCK_METHOD(
|
||||
SendBufferReturnType,
|
||||
sendBuffer,
|
||||
(boost::asio::const_buffer, boost::asio::yield_context, std::chrono::steady_clock::duration),
|
||||
(override)
|
||||
);
|
||||
};
|
||||
|
||||
using MockWsConnection = testing::NiceMock<MockWsConnectionImpl>;
|
||||
using MockWsConnectionPtr = std::unique_ptr<testing::NiceMock<MockWsConnectionImpl>>;
|
||||
|
||||
using StrictMockWsConnection = testing::StrictMock<MockWsConnectionImpl>;
|
||||
using StrictMockWsConnectionPtr = std::unique_ptr<testing::StrictMock<MockWsConnectionImpl>>;
|
||||
@@ -134,19 +134,24 @@ target_sources(
|
||||
util/TxUtilTests.cpp
|
||||
util/WithTimeout.cpp
|
||||
# Webserver
|
||||
web/AdminVerificationTests.cpp
|
||||
web/dosguard/DOSGuardTests.cpp
|
||||
web/dosguard/IntervalSweepHandlerTests.cpp
|
||||
web/dosguard/WhitelistHandlerTests.cpp
|
||||
web/impl/AdminVerificationTests.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/RPCServerHandlerTests.cpp
|
||||
web/ServerTests.cpp
|
||||
web/SubscriptionContextTests.cpp
|
||||
# New Config
|
||||
util/newconfig/ArrayTests.cpp
|
||||
util/newconfig/ArrayViewTests.cpp
|
||||
|
||||
@@ -41,11 +41,27 @@ TEST_F(CliArgsTests, Parse_NoArgs)
|
||||
int const returnCode = 123;
|
||||
EXPECT_CALL(onRunMock, Call).WillOnce([](CliArgs::Action::Run const& run) {
|
||||
EXPECT_EQ(run.configPath, CliArgs::defaultConfigPath);
|
||||
EXPECT_FALSE(run.useNgWebServer);
|
||||
return returnCode;
|
||||
});
|
||||
EXPECT_EQ(action.apply(onRunMock.AsStdFunction(), onExitMock.AsStdFunction()), returnCode);
|
||||
}
|
||||
|
||||
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::defaultConfigPath);
|
||||
EXPECT_TRUE(run.useNgWebServer);
|
||||
return returnCode;
|
||||
});
|
||||
EXPECT_EQ(action.apply(onRunMock.AsStdFunction(), onExitMock.AsStdFunction()), returnCode);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CliArgsTests, Parse_VersionHelp)
|
||||
{
|
||||
for (auto& argv :
|
||||
|
||||
@@ -41,6 +41,7 @@ using FeedBookChangeTest = FeedBaseTest<BookChangesFeed>;
|
||||
|
||||
TEST_F(FeedBookChangeTest, Pub)
|
||||
{
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->count(), 1);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "feed/FeedTestUtil.hpp"
|
||||
#include "feed/impl/ForwardFeed.hpp"
|
||||
#include "util/async/AnyExecutionContext.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
@@ -44,11 +45,14 @@ using FeedForwardTest = FeedBaseTest<NamedForwardFeedTest>;
|
||||
|
||||
TEST_F(FeedForwardTest, Pub)
|
||||
{
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->count(), 1);
|
||||
|
||||
auto const json = json::parse(FEED).as_object();
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED))).Times(1);
|
||||
testFeedPtr->pub(json);
|
||||
|
||||
testFeedPtr->unsub(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->count(), 0);
|
||||
testFeedPtr->pub(json);
|
||||
@@ -56,12 +60,18 @@ TEST_F(FeedForwardTest, Pub)
|
||||
|
||||
TEST_F(FeedForwardTest, AutoDisconnect)
|
||||
{
|
||||
web::SubscriptionContextInterface::OnDisconnectSlot slot;
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect).WillOnce(testing::SaveArg<0>(&slot));
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->count(), 1);
|
||||
|
||||
auto const json = json::parse(FEED).as_object();
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED))).Times(1);
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED)));
|
||||
testFeedPtr->pub(json);
|
||||
|
||||
slot(sessionPtr.get());
|
||||
sessionPtr.reset();
|
||||
EXPECT_EQ(testFeedPtr->count(), 0);
|
||||
|
||||
testFeedPtr->pub(json);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "feed/FeedTestUtil.hpp"
|
||||
#include "feed/impl/LedgerFeed.hpp"
|
||||
#include "util/TestObject.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
@@ -60,6 +61,7 @@ TEST_F(FeedLedgerTest, SubPub)
|
||||
})";
|
||||
boost::asio::io_context ioContext;
|
||||
boost::asio::spawn(ioContext, [this](boost::asio::yield_context yield) {
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
auto res = testFeedPtr->sub(yield, backend, sessionPtr);
|
||||
// check the response
|
||||
EXPECT_EQ(res, json::parse(LedgerResponse));
|
||||
@@ -112,6 +114,10 @@ TEST_F(FeedLedgerTest, AutoDisconnect)
|
||||
"reserve_base":3,
|
||||
"reserve_inc":2
|
||||
})";
|
||||
|
||||
web::SubscriptionContextInterface::OnDisconnectSlot slot;
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect).WillOnce(testing::SaveArg<0>(&slot));
|
||||
|
||||
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
|
||||
auto res = testFeedPtr->sub(yield, backend, sessionPtr);
|
||||
// check the response
|
||||
@@ -120,7 +126,10 @@ TEST_F(FeedLedgerTest, AutoDisconnect)
|
||||
EXPECT_EQ(testFeedPtr->count(), 1);
|
||||
EXPECT_CALL(*mockSessionPtr, send(_)).Times(0);
|
||||
|
||||
ASSERT_TRUE(slot);
|
||||
slot(sessionPtr.get());
|
||||
sessionPtr.reset();
|
||||
|
||||
EXPECT_EQ(testFeedPtr->count(), 0);
|
||||
|
||||
auto const ledgerHeader2 = CreateLedgerHeader(LEDGERHASH, 31);
|
||||
|
||||
@@ -24,13 +24,15 @@
|
||||
#include "util/SyncExecutionCtxFixture.hpp"
|
||||
#include "util/TestObject.hpp"
|
||||
#include "util/prometheus/Gauge.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
constexpr static auto ACCOUNT1 = "rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb";
|
||||
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
|
||||
@@ -60,10 +62,11 @@ using FeedProposedTransactionTest = FeedBaseTest<ProposedTransactionFeed>;
|
||||
|
||||
TEST_F(FeedProposedTransactionTest, ProposedTransaction)
|
||||
{
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION)));
|
||||
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
|
||||
|
||||
testFeedPtr->unsub(sessionPtr);
|
||||
@@ -75,15 +78,19 @@ TEST_F(FeedProposedTransactionTest, ProposedTransaction)
|
||||
TEST_F(FeedProposedTransactionTest, AccountProposedTransaction)
|
||||
{
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(account, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
|
||||
|
||||
std::shared_ptr<web::ConnectionBase> const sessionIdle = std::make_shared<MockSession>();
|
||||
web::SubscriptionContextPtr const sessionIdle = std::make_shared<MockSession>();
|
||||
auto const accountIdle = GetAccountIDWithString(ACCOUNT3);
|
||||
|
||||
EXPECT_CALL(*dynamic_cast<MockSession*>(sessionIdle.get()), onDisconnect);
|
||||
testFeedPtr->sub(accountIdle, sessionIdle);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION)));
|
||||
|
||||
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
|
||||
|
||||
@@ -97,8 +104,11 @@ TEST_F(FeedProposedTransactionTest, AccountProposedTransaction)
|
||||
TEST_F(FeedProposedTransactionTest, SubStreamAndAccount)
|
||||
{
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect).Times(2);
|
||||
testFeedPtr->sub(account, sessionPtr);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(2);
|
||||
@@ -108,7 +118,7 @@ TEST_F(FeedProposedTransactionTest, SubStreamAndAccount)
|
||||
// unsub
|
||||
testFeedPtr->unsub(account, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION)));
|
||||
|
||||
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
|
||||
|
||||
@@ -124,17 +134,18 @@ TEST_F(FeedProposedTransactionTest, AccountProposedTransactionDuplicate)
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
auto const account2 = GetAccountIDWithString(ACCOUNT2);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect).Times(2);
|
||||
testFeedPtr->sub(account, sessionPtr);
|
||||
testFeedPtr->sub(account2, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION)));
|
||||
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
|
||||
|
||||
// unsub account1
|
||||
testFeedPtr->unsub(account, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION)));
|
||||
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
|
||||
|
||||
// unsub account2
|
||||
@@ -146,24 +157,33 @@ TEST_F(FeedProposedTransactionTest, AccountProposedTransactionDuplicate)
|
||||
|
||||
TEST_F(FeedProposedTransactionTest, Count)
|
||||
{
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
// repeat
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
|
||||
|
||||
auto const account1 = GetAccountIDWithString(ACCOUNT1);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(account1, sessionPtr);
|
||||
// repeat
|
||||
testFeedPtr->sub(account1, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
|
||||
|
||||
auto const sessionPtr2 = std::make_shared<MockSession>();
|
||||
|
||||
EXPECT_CALL(*dynamic_cast<MockSession*>(sessionPtr2.get()), onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr2);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubcount(), 2);
|
||||
|
||||
auto const account2 = GetAccountIDWithString(ACCOUNT2);
|
||||
|
||||
EXPECT_CALL(*dynamic_cast<MockSession*>(sessionPtr2.get()), onDisconnect);
|
||||
testFeedPtr->sub(account2, sessionPtr2);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
|
||||
|
||||
EXPECT_CALL(*dynamic_cast<MockSession*>(sessionPtr2.get()), onDisconnect);
|
||||
testFeedPtr->sub(account1, sessionPtr2);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 3);
|
||||
|
||||
@@ -184,31 +204,51 @@ TEST_F(FeedProposedTransactionTest, Count)
|
||||
|
||||
TEST_F(FeedProposedTransactionTest, AutoDisconnect)
|
||||
{
|
||||
std::vector<web::SubscriptionContextInterface::OnDisconnectSlot> sessionOnDisconnectSlots;
|
||||
ON_CALL(*mockSessionPtr, onDisconnect).WillByDefault([&sessionOnDisconnectSlots](auto slot) {
|
||||
sessionOnDisconnectSlots.push_back(slot);
|
||||
});
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
// repeat
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
|
||||
|
||||
auto const account1 = GetAccountIDWithString(ACCOUNT1);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(account1, sessionPtr);
|
||||
// repeat
|
||||
testFeedPtr->sub(account1, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
|
||||
|
||||
auto sessionPtr2 = std::make_shared<MockSession>();
|
||||
auto mockSessionPtr2 = dynamic_cast<MockSession*>(sessionPtr2.get());
|
||||
std::vector<web::SubscriptionContextInterface::OnDisconnectSlot> session2OnDisconnectSlots;
|
||||
ON_CALL(*mockSessionPtr2, onDisconnect).WillByDefault([&session2OnDisconnectSlots](auto slot) {
|
||||
session2OnDisconnectSlots.push_back(slot);
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr2, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr2);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubcount(), 2);
|
||||
|
||||
auto const account2 = GetAccountIDWithString(ACCOUNT2);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr2, onDisconnect);
|
||||
testFeedPtr->sub(account2, sessionPtr2);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr2, onDisconnect);
|
||||
testFeedPtr->sub(account1, sessionPtr2);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 3);
|
||||
|
||||
std::ranges::for_each(session2OnDisconnectSlots, [&sessionPtr2](auto& slot) { slot(sessionPtr2.get()); });
|
||||
sessionPtr2.reset();
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
|
||||
|
||||
std::ranges::for_each(sessionOnDisconnectSlots, [this](auto& slot) { slot(sessionPtr.get()); });
|
||||
sessionPtr.reset();
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubcount(), 0);
|
||||
@@ -216,21 +256,9 @@ TEST_F(FeedProposedTransactionTest, AutoDisconnect)
|
||||
|
||||
struct ProposedTransactionFeedMockPrometheusTest : WithMockPrometheus, SyncExecutionCtxFixture {
|
||||
protected:
|
||||
std::shared_ptr<web::ConnectionBase> sessionPtr;
|
||||
std::shared_ptr<ProposedTransactionFeed> testFeedPtr;
|
||||
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
testFeedPtr = std::make_shared<ProposedTransactionFeed>(ctx);
|
||||
sessionPtr = std::make_shared<MockSession>();
|
||||
}
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
sessionPtr.reset();
|
||||
testFeedPtr.reset();
|
||||
}
|
||||
web::SubscriptionContextPtr sessionPtr = std::make_shared<MockSession>();
|
||||
std::shared_ptr<ProposedTransactionFeed> testFeedPtr = std::make_shared<ProposedTransactionFeed>(ctx);
|
||||
MockSession* mockSessionPtr = dynamic_cast<MockSession*>(sessionPtr.get());
|
||||
};
|
||||
|
||||
TEST_F(ProposedTransactionFeedMockPrometheusTest, subUnsub)
|
||||
@@ -243,10 +271,12 @@ TEST_F(ProposedTransactionFeedMockPrometheusTest, subUnsub)
|
||||
EXPECT_CALL(counterAccount, add(1));
|
||||
EXPECT_CALL(counterAccount, add(-1));
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
testFeedPtr->unsub(sessionPtr);
|
||||
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(account, sessionPtr);
|
||||
testFeedPtr->unsub(account, sessionPtr);
|
||||
}
|
||||
@@ -256,15 +286,24 @@ TEST_F(ProposedTransactionFeedMockPrometheusTest, AutoDisconnect)
|
||||
auto& counterTx = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"tx_proposed\"}");
|
||||
auto& counterAccount = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"account_proposed\"}");
|
||||
|
||||
std::vector<web::SubscriptionContextInterface::OnDisconnectSlot> sessionOnDisconnectSlots;
|
||||
|
||||
EXPECT_CALL(counterTx, add(1));
|
||||
EXPECT_CALL(counterTx, add(-1));
|
||||
EXPECT_CALL(counterAccount, add(1));
|
||||
EXPECT_CALL(counterAccount, add(-1));
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect).WillOnce([&sessionOnDisconnectSlots](auto slot) {
|
||||
sessionOnDisconnectSlots.push_back(slot);
|
||||
});
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect).WillOnce([&sessionOnDisconnectSlots](auto slot) {
|
||||
sessionOnDisconnectSlots.push_back(slot);
|
||||
});
|
||||
testFeedPtr->sub(account, sessionPtr);
|
||||
|
||||
std::ranges::for_each(sessionOnDisconnectSlots, [this](auto& slot) { slot(sessionPtr.get()); });
|
||||
sessionPtr.reset();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#include "util/SyncExecutionCtxFixture.hpp"
|
||||
#include "util/async/AnyExecutionContext.hpp"
|
||||
#include "util/prometheus/Gauge.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
@@ -38,23 +38,9 @@ using namespace util::prometheus;
|
||||
|
||||
struct FeedBaseMockPrometheusTest : WithMockPrometheus, SyncExecutionCtxFixture {
|
||||
protected:
|
||||
std::shared_ptr<web::ConnectionBase> sessionPtr;
|
||||
std::shared_ptr<SingleFeedBase> testFeedPtr;
|
||||
MockSession* mockSessionPtr = nullptr;
|
||||
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
testFeedPtr = std::make_shared<SingleFeedBase>(ctx, "testFeed");
|
||||
sessionPtr = std::make_shared<MockSession>();
|
||||
mockSessionPtr = dynamic_cast<MockSession*>(sessionPtr.get());
|
||||
}
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
sessionPtr.reset();
|
||||
testFeedPtr.reset();
|
||||
}
|
||||
web::SubscriptionContextPtr sessionPtr = std::make_shared<MockSession>();
|
||||
std::shared_ptr<SingleFeedBase> testFeedPtr = std::make_shared<SingleFeedBase>(ctx, "testFeed");
|
||||
MockSession* mockSessionPtr = dynamic_cast<MockSession*>(sessionPtr.get());
|
||||
};
|
||||
|
||||
TEST_F(FeedBaseMockPrometheusTest, subUnsub)
|
||||
@@ -63,6 +49,7 @@ TEST_F(FeedBaseMockPrometheusTest, subUnsub)
|
||||
EXPECT_CALL(counter, add(1));
|
||||
EXPECT_CALL(counter, add(-1));
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
testFeedPtr->unsub(sessionPtr);
|
||||
}
|
||||
@@ -73,7 +60,10 @@ TEST_F(FeedBaseMockPrometheusTest, AutoUnsub)
|
||||
EXPECT_CALL(counter, add(1));
|
||||
EXPECT_CALL(counter, add(-1));
|
||||
|
||||
web::SubscriptionContextInterface::OnDisconnectSlot slot;
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect).WillOnce(testing::SaveArg<0>(&slot));
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
slot(sessionPtr.get());
|
||||
sessionPtr.reset();
|
||||
}
|
||||
|
||||
@@ -88,7 +78,8 @@ using SingleFeedBaseTest = FeedBaseTest<NamedSingleFeedTest>;
|
||||
|
||||
TEST_F(SingleFeedBaseTest, Test)
|
||||
{
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED))).Times(1);
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED)));
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->count(), 1);
|
||||
testFeedPtr->pub(FEED);
|
||||
@@ -100,17 +91,21 @@ TEST_F(SingleFeedBaseTest, Test)
|
||||
|
||||
TEST_F(SingleFeedBaseTest, TestAutoDisconnect)
|
||||
{
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED))).Times(1);
|
||||
web::SubscriptionContextInterface::OnDisconnectSlot slot;
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect).WillOnce(testing::SaveArg<0>(&slot));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED)));
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->count(), 1);
|
||||
testFeedPtr->pub(FEED);
|
||||
|
||||
slot(sessionPtr.get());
|
||||
sessionPtr.reset();
|
||||
EXPECT_EQ(testFeedPtr->count(), 0);
|
||||
}
|
||||
|
||||
TEST_F(SingleFeedBaseTest, RepeatSub)
|
||||
{
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->count(), 1);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
|
||||
@@ -20,13 +20,14 @@
|
||||
#include "data/Types.hpp"
|
||||
#include "feed/FeedTestUtil.hpp"
|
||||
#include "feed/SubscriptionManager.hpp"
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/MockBackendTestFixture.hpp"
|
||||
#include "util/MockPrometheus.hpp"
|
||||
#include "util/MockWsBase.hpp"
|
||||
#include "util/TestObject.hpp"
|
||||
#include "util/async/context/BasicExecutionContext.hpp"
|
||||
#include "util/async/context/SyncExecutionContext.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
@@ -39,8 +40,8 @@
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
constexpr static auto ACCOUNT1 = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
|
||||
@@ -56,25 +57,15 @@ using namespace feed::impl;
|
||||
template <class Execution>
|
||||
class SubscriptionManagerBaseTest : public util::prometheus::WithPrometheus, public MockBackendTest {
|
||||
protected:
|
||||
std::shared_ptr<SubscriptionManager> subscriptionManagerPtr;
|
||||
std::shared_ptr<web::ConnectionBase> session;
|
||||
MockSession* sessionPtr = nullptr;
|
||||
|
||||
void
|
||||
SetUp() override
|
||||
SubscriptionManagerBaseTest()
|
||||
{
|
||||
subscriptionManagerPtr = std::make_shared<SubscriptionManager>(Execution(2), backend);
|
||||
session = std::make_shared<MockSession>();
|
||||
session->apiSubVersion = 1;
|
||||
sessionPtr = dynamic_cast<MockSession*>(session.get());
|
||||
ASSERT(sessionPtr != nullptr, "dynamic_cast failed");
|
||||
}
|
||||
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
session.reset();
|
||||
subscriptionManagerPtr.reset();
|
||||
}
|
||||
std::shared_ptr<SubscriptionManager> subscriptionManagerPtr =
|
||||
std::make_shared<SubscriptionManager>(Execution(2), backend);
|
||||
web::SubscriptionContextPtr session = std::make_shared<MockSession>();
|
||||
MockSession* sessionPtr = dynamic_cast<MockSession*>(session.get());
|
||||
};
|
||||
|
||||
using SubscriptionManagerTest = SubscriptionManagerBaseTest<util::async::SyncExecutionContext>;
|
||||
@@ -83,7 +74,9 @@ using SubscriptionManagerAsyncTest = SubscriptionManagerBaseTest<util::async::Po
|
||||
|
||||
TEST_F(SubscriptionManagerAsyncTest, MultipleThreadCtx)
|
||||
{
|
||||
EXPECT_CALL(*sessionPtr, onDisconnect);
|
||||
subscriptionManagerPtr->subManifest(session);
|
||||
EXPECT_CALL(*sessionPtr, onDisconnect);
|
||||
subscriptionManagerPtr->subValidation(session);
|
||||
|
||||
constexpr static auto jsonManifest = R"({"manifest":"test"})";
|
||||
@@ -97,7 +90,9 @@ TEST_F(SubscriptionManagerAsyncTest, MultipleThreadCtx)
|
||||
|
||||
TEST_F(SubscriptionManagerAsyncTest, MultipleThreadCtxSessionDieEarly)
|
||||
{
|
||||
EXPECT_CALL(*sessionPtr, onDisconnect);
|
||||
subscriptionManagerPtr->subManifest(session);
|
||||
EXPECT_CALL(*sessionPtr, onDisconnect);
|
||||
subscriptionManagerPtr->subValidation(session);
|
||||
|
||||
EXPECT_CALL(*sessionPtr, send(testing::_)).Times(0);
|
||||
@@ -121,8 +116,18 @@ TEST_F(SubscriptionManagerTest, ReportCurrentSubscriber)
|
||||
"books":2,
|
||||
"book_changes":2
|
||||
})";
|
||||
std::shared_ptr<web::ConnectionBase> const session1 = std::make_shared<MockSession>();
|
||||
std::shared_ptr<web::ConnectionBase> session2 = std::make_shared<MockSession>();
|
||||
web::SubscriptionContextPtr const session1 = std::make_shared<MockSession>();
|
||||
MockSession* mockSession1 = dynamic_cast<MockSession*>(session1.get());
|
||||
|
||||
web::SubscriptionContextPtr session2 = std::make_shared<MockSession>();
|
||||
MockSession* mockSession2 = dynamic_cast<MockSession*>(session2.get());
|
||||
std::vector<web::SubscriptionContextInterface::OnDisconnectSlot> session2OnDisconnectSlots;
|
||||
ON_CALL(*mockSession2, onDisconnect).WillByDefault([&session2OnDisconnectSlots](auto slot) {
|
||||
session2OnDisconnectSlots.push_back(slot);
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mockSession1, onDisconnect).Times(5);
|
||||
EXPECT_CALL(*mockSession2, onDisconnect).Times(4);
|
||||
subscriptionManagerPtr->subBookChanges(session1);
|
||||
subscriptionManagerPtr->subBookChanges(session2);
|
||||
subscriptionManagerPtr->subManifest(session1);
|
||||
@@ -130,7 +135,10 @@ TEST_F(SubscriptionManagerTest, ReportCurrentSubscriber)
|
||||
subscriptionManagerPtr->subProposedTransactions(session1);
|
||||
subscriptionManagerPtr->subProposedTransactions(session2);
|
||||
subscriptionManagerPtr->subTransactions(session1);
|
||||
session2->apiSubVersion = 2;
|
||||
|
||||
// session2->apiSubVersion = 2;
|
||||
EXPECT_CALL(*mockSession1, onDisconnect).Times(5);
|
||||
EXPECT_CALL(*mockSession2, onDisconnect).Times(6);
|
||||
subscriptionManagerPtr->subTransactions(session2);
|
||||
subscriptionManagerPtr->subValidation(session1);
|
||||
subscriptionManagerPtr->subValidation(session2);
|
||||
@@ -172,6 +180,7 @@ TEST_F(SubscriptionManagerTest, ReportCurrentSubscriber)
|
||||
checkResult(subscriptionManagerPtr->report(), 1);
|
||||
|
||||
// count down when session disconnect
|
||||
std::ranges::for_each(session2OnDisconnectSlots, [&session2](auto& slot) { slot(session2.get()); });
|
||||
session2.reset();
|
||||
checkResult(subscriptionManagerPtr->report(), 0);
|
||||
}
|
||||
@@ -179,7 +188,8 @@ TEST_F(SubscriptionManagerTest, ReportCurrentSubscriber)
|
||||
TEST_F(SubscriptionManagerTest, ManifestTest)
|
||||
{
|
||||
constexpr static auto dummyManifest = R"({"manifest":"test"})";
|
||||
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(dummyManifest))).Times(1);
|
||||
EXPECT_CALL(*sessionPtr, onDisconnect);
|
||||
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(dummyManifest)));
|
||||
subscriptionManagerPtr->subManifest(session);
|
||||
subscriptionManagerPtr->forwardManifest(json::parse(dummyManifest).get_object());
|
||||
|
||||
@@ -191,7 +201,8 @@ TEST_F(SubscriptionManagerTest, ManifestTest)
|
||||
TEST_F(SubscriptionManagerTest, ValidationTest)
|
||||
{
|
||||
constexpr static auto dummy = R"({"validation":"test"})";
|
||||
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(dummy))).Times(1);
|
||||
EXPECT_CALL(*sessionPtr, onDisconnect);
|
||||
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(dummy)));
|
||||
subscriptionManagerPtr->subValidation(session);
|
||||
subscriptionManagerPtr->forwardValidation(json::parse(dummy).get_object());
|
||||
|
||||
@@ -202,6 +213,7 @@ TEST_F(SubscriptionManagerTest, ValidationTest)
|
||||
|
||||
TEST_F(SubscriptionManagerTest, BookChangesTest)
|
||||
{
|
||||
EXPECT_CALL(*sessionPtr, onDisconnect);
|
||||
subscriptionManagerPtr->subBookChanges(session);
|
||||
EXPECT_EQ(subscriptionManagerPtr->report()["book_changes"], 1);
|
||||
|
||||
@@ -234,7 +246,7 @@ TEST_F(SubscriptionManagerTest, BookChangesTest)
|
||||
}
|
||||
]
|
||||
})";
|
||||
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(bookChangePublish))).Times(1);
|
||||
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(bookChangePublish)));
|
||||
|
||||
subscriptionManagerPtr->pubBookChanges(ledgerHeader, transactions);
|
||||
|
||||
@@ -266,6 +278,7 @@ TEST_F(SubscriptionManagerTest, LedgerTest)
|
||||
})";
|
||||
boost::asio::io_context ctx;
|
||||
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
|
||||
EXPECT_CALL(*sessionPtr, onDisconnect);
|
||||
auto const res = subscriptionManagerPtr->subLedger(yield, session);
|
||||
// check the response
|
||||
EXPECT_EQ(res, json::parse(LedgerResponse));
|
||||
@@ -289,7 +302,7 @@ TEST_F(SubscriptionManagerTest, LedgerTest)
|
||||
"validated_ledgers":"10-31",
|
||||
"txn_count":8
|
||||
})";
|
||||
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(ledgerPub))).Times(1);
|
||||
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(ledgerPub)));
|
||||
subscriptionManagerPtr->pubLedger(ledgerHeader2, fee2, "10-31", 8);
|
||||
|
||||
// test unsub
|
||||
@@ -302,6 +315,7 @@ TEST_F(SubscriptionManagerTest, TransactionTest)
|
||||
auto const issue1 = GetIssue(CURRENCY, ISSUER);
|
||||
auto const account = GetAccountIDWithString(ISSUER);
|
||||
ripple::Book const book{ripple::xrpIssue(), issue1};
|
||||
EXPECT_CALL(*sessionPtr, onDisconnect).Times(3);
|
||||
subscriptionManagerPtr->subBook(book, session);
|
||||
subscriptionManagerPtr->subTransactions(session);
|
||||
subscriptionManagerPtr->subAccount(account, session);
|
||||
@@ -378,6 +392,7 @@ TEST_F(SubscriptionManagerTest, TransactionTest)
|
||||
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
|
||||
})";
|
||||
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(OrderbookPublish))).Times(3);
|
||||
EXPECT_CALL(*sessionPtr, apiSubversion).Times(3).WillRepeatedly(testing::Return(1));
|
||||
subscriptionManagerPtr->pubTransaction(trans1, ledgerHeader);
|
||||
|
||||
subscriptionManagerPtr->unsubBook(book, session);
|
||||
@@ -391,6 +406,7 @@ TEST_F(SubscriptionManagerTest, TransactionTest)
|
||||
TEST_F(SubscriptionManagerTest, ProposedTransactionTest)
|
||||
{
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
EXPECT_CALL(*sessionPtr, onDisconnect).Times(4);
|
||||
subscriptionManagerPtr->subProposedAccount(account, session);
|
||||
subscriptionManagerPtr->subProposedTransactions(session);
|
||||
EXPECT_EQ(subscriptionManagerPtr->report()["accounts_proposed"], 1);
|
||||
@@ -476,6 +492,7 @@ TEST_F(SubscriptionManagerTest, ProposedTransactionTest)
|
||||
|
||||
auto const metaObj = CreateMetaDataForBookChange(CURRENCY, ACCOUNT1, 22, 3, 1, 1, 3);
|
||||
trans1.metadata = metaObj.getSerializer().peekData();
|
||||
EXPECT_CALL(*sessionPtr, apiSubversion).Times(2).WillRepeatedly(testing::Return(1));
|
||||
subscriptionManagerPtr->pubTransaction(trans1, ledgerHeader);
|
||||
|
||||
// unsub account1
|
||||
@@ -487,6 +504,7 @@ TEST_F(SubscriptionManagerTest, ProposedTransactionTest)
|
||||
|
||||
TEST_F(SubscriptionManagerTest, DuplicateResponseSubTxAndProposedTx)
|
||||
{
|
||||
EXPECT_CALL(*sessionPtr, onDisconnect).Times(3);
|
||||
subscriptionManagerPtr->subProposedTransactions(session);
|
||||
subscriptionManagerPtr->subTransactions(session);
|
||||
EXPECT_EQ(subscriptionManagerPtr->report()["transactions"], 1);
|
||||
@@ -502,6 +520,7 @@ TEST_F(SubscriptionManagerTest, DuplicateResponseSubTxAndProposedTx)
|
||||
|
||||
auto const metaObj = CreateMetaDataForBookChange(CURRENCY, ACCOUNT1, 22, 3, 1, 1, 3);
|
||||
trans1.metadata = metaObj.getSerializer().peekData();
|
||||
EXPECT_CALL(*sessionPtr, apiSubversion).Times(2).WillRepeatedly(testing::Return(1));
|
||||
subscriptionManagerPtr->pubTransaction(trans1, ledgerHeader);
|
||||
|
||||
subscriptionManagerPtr->unsubTransactions(session);
|
||||
@@ -513,12 +532,13 @@ TEST_F(SubscriptionManagerTest, DuplicateResponseSubTxAndProposedTx)
|
||||
TEST_F(SubscriptionManagerTest, NoDuplicateResponseSubAccountAndProposedAccount)
|
||||
{
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
EXPECT_CALL(*sessionPtr, onDisconnect).Times(3);
|
||||
subscriptionManagerPtr->subProposedAccount(account, session);
|
||||
subscriptionManagerPtr->subAccount(account, session);
|
||||
EXPECT_EQ(subscriptionManagerPtr->report()["accounts_proposed"], 1);
|
||||
EXPECT_EQ(subscriptionManagerPtr->report()["account"], 1);
|
||||
|
||||
EXPECT_CALL(*sessionPtr, send(testing::_)).Times(1);
|
||||
EXPECT_CALL(*sessionPtr, send(testing::_));
|
||||
|
||||
auto const ledgerHeader = CreateLedgerHeader(LEDGERHASH, 33);
|
||||
auto trans1 = TransactionAndMetadata();
|
||||
@@ -528,6 +548,7 @@ TEST_F(SubscriptionManagerTest, NoDuplicateResponseSubAccountAndProposedAccount)
|
||||
|
||||
auto const metaObj = CreateMetaDataForBookChange(CURRENCY, ACCOUNT1, 22, 3, 1, 1, 3);
|
||||
trans1.metadata = metaObj.getSerializer().peekData();
|
||||
EXPECT_CALL(*sessionPtr, apiSubversion).WillRepeatedly(testing::Return(1));
|
||||
subscriptionManagerPtr->pubTransaction(trans1, ledgerHeader);
|
||||
|
||||
// unsub account1
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#include "feed/impl/TrackableSignal.hpp"
|
||||
#include "feed/impl/TrackableSignalMap.hpp"
|
||||
#include "util/MockWsBase.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
@@ -30,24 +30,12 @@
|
||||
using namespace testing;
|
||||
|
||||
struct FeedTrackableSignalTests : Test {
|
||||
protected:
|
||||
std::shared_ptr<web::ConnectionBase> sessionPtr;
|
||||
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
sessionPtr = std::make_shared<MockSession>();
|
||||
}
|
||||
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
}
|
||||
web::SubscriptionContextPtr sessionPtr = std::make_shared<MockSession>();
|
||||
};
|
||||
|
||||
TEST_F(FeedTrackableSignalTests, Connect)
|
||||
{
|
||||
feed::impl::TrackableSignal<web::ConnectionBase, std::string> signal;
|
||||
feed::impl::TrackableSignal<web::SubscriptionContextInterface, std::string> signal;
|
||||
std::string testString;
|
||||
auto const slot = [&](std::string const& s) { testString += s; };
|
||||
EXPECT_TRUE(signal.connectTrackableSlot(sessionPtr, slot));
|
||||
@@ -69,7 +57,7 @@ TEST_F(FeedTrackableSignalTests, Connect)
|
||||
|
||||
TEST_F(FeedTrackableSignalTests, AutoDisconnect)
|
||||
{
|
||||
feed::impl::TrackableSignal<web::ConnectionBase, std::string> signal;
|
||||
feed::impl::TrackableSignal<web::SubscriptionContextInterface, std::string> signal;
|
||||
std::string testString;
|
||||
auto const slot = [&](std::string const& s) { testString += s; };
|
||||
EXPECT_TRUE(signal.connectTrackableSlot(sessionPtr, slot));
|
||||
@@ -91,7 +79,7 @@ TEST_F(FeedTrackableSignalTests, AutoDisconnect)
|
||||
|
||||
TEST_F(FeedTrackableSignalTests, MapConnect)
|
||||
{
|
||||
feed::impl::TrackableSignalMap<std::string, web::ConnectionBase, std::string> signalMap;
|
||||
feed::impl::TrackableSignalMap<std::string, web::SubscriptionContextInterface, std::string> signalMap;
|
||||
std::string testString;
|
||||
auto const slot = [&](std::string const& s) { testString += s; };
|
||||
EXPECT_TRUE(signalMap.connectTrackableSlot(sessionPtr, "test", slot));
|
||||
@@ -115,7 +103,7 @@ TEST_F(FeedTrackableSignalTests, MapConnect)
|
||||
|
||||
TEST_F(FeedTrackableSignalTests, MapAutoDisconnect)
|
||||
{
|
||||
feed::impl::TrackableSignalMap<std::string, web::ConnectionBase, std::string> signalMap;
|
||||
feed::impl::TrackableSignalMap<std::string, web::SubscriptionContextInterface, std::string> signalMap;
|
||||
std::string testString;
|
||||
auto const slot = [&](std::string const& s) { testString += s; };
|
||||
EXPECT_TRUE(signalMap.connectTrackableSlot(sessionPtr, "test", slot));
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "util/SyncExecutionCtxFixture.hpp"
|
||||
#include "util/TestObject.hpp"
|
||||
#include "util/prometheus/Gauge.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
@@ -39,7 +39,9 @@
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
constexpr static auto ACCOUNT1 = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
|
||||
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
|
||||
@@ -164,6 +166,7 @@ using FeedTransactionTest = FeedBaseTest<TransactionFeed>;
|
||||
|
||||
TEST_F(FeedTransactionTest, SubTransactionV1)
|
||||
{
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubCount(), 1);
|
||||
|
||||
@@ -173,7 +176,9 @@ TEST_F(FeedTransactionTest, SubTransactionV1)
|
||||
trans1.transaction = obj.getSerializer().peekData();
|
||||
trans1.ledgerSequence = 32;
|
||||
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1)));
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
testFeedPtr->unsub(sessionPtr);
|
||||
@@ -183,6 +188,7 @@ TEST_F(FeedTransactionTest, SubTransactionV1)
|
||||
|
||||
TEST_F(FeedTransactionTest, SubTransactionForProposedTx)
|
||||
{
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->subProposed(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubCount(), 0);
|
||||
|
||||
@@ -193,7 +199,8 @@ TEST_F(FeedTransactionTest, SubTransactionForProposedTx)
|
||||
trans1.ledgerSequence = 32;
|
||||
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1)));
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
testFeedPtr->unsubProposed(sessionPtr);
|
||||
@@ -202,7 +209,7 @@ TEST_F(FeedTransactionTest, SubTransactionForProposedTx)
|
||||
|
||||
TEST_F(FeedTransactionTest, SubTransactionV2)
|
||||
{
|
||||
sessionPtr->apiSubVersion = 2;
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubCount(), 1);
|
||||
|
||||
@@ -213,7 +220,8 @@ TEST_F(FeedTransactionTest, SubTransactionV2)
|
||||
trans1.ledgerSequence = 32;
|
||||
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2)));
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
testFeedPtr->unsub(sessionPtr);
|
||||
@@ -225,6 +233,8 @@ TEST_F(FeedTransactionTest, SubTransactionV2)
|
||||
TEST_F(FeedTransactionTest, SubAccountV1)
|
||||
{
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(account, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
|
||||
|
||||
@@ -236,7 +246,8 @@ TEST_F(FeedTransactionTest, SubAccountV1)
|
||||
trans1.ledgerSequence = 32;
|
||||
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1)));
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
testFeedPtr->unsub(account, sessionPtr);
|
||||
@@ -248,6 +259,8 @@ TEST_F(FeedTransactionTest, SubAccountV1)
|
||||
TEST_F(FeedTransactionTest, SubForProposedAccount)
|
||||
{
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->subProposed(account, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
|
||||
|
||||
@@ -259,7 +272,8 @@ TEST_F(FeedTransactionTest, SubForProposedAccount)
|
||||
trans1.ledgerSequence = 32;
|
||||
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1)));
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
testFeedPtr->unsubProposed(account, sessionPtr);
|
||||
@@ -269,7 +283,7 @@ TEST_F(FeedTransactionTest, SubForProposedAccount)
|
||||
TEST_F(FeedTransactionTest, SubAccountV2)
|
||||
{
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
sessionPtr->apiSubVersion = 2;
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(account, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
|
||||
|
||||
@@ -281,7 +295,8 @@ TEST_F(FeedTransactionTest, SubAccountV2)
|
||||
trans1.ledgerSequence = 32;
|
||||
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2)));
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
testFeedPtr->unsub(account, sessionPtr);
|
||||
@@ -293,7 +308,7 @@ TEST_F(FeedTransactionTest, SubAccountV2)
|
||||
TEST_F(FeedTransactionTest, SubBothTransactionAndAccount)
|
||||
{
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
sessionPtr->apiSubVersion = 2;
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect).Times(2);
|
||||
testFeedPtr->sub(account, sessionPtr);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
|
||||
@@ -307,6 +322,7 @@ TEST_F(FeedTransactionTest, SubBothTransactionAndAccount)
|
||||
trans1.ledgerSequence = 32;
|
||||
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).Times(2).WillRepeatedly(testing::Return(2));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(2);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
@@ -322,6 +338,8 @@ TEST_F(FeedTransactionTest, SubBookV1)
|
||||
{
|
||||
auto const issue1 = GetIssue(CURRENCY, ISSUER);
|
||||
ripple::Book const book{ripple::xrpIssue(), issue1};
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(book, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->bookSubCount(), 1);
|
||||
|
||||
@@ -394,6 +412,7 @@ TEST_F(FeedTransactionTest, SubBookV1)
|
||||
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
|
||||
})";
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(OrderbookPublish))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
@@ -448,6 +467,8 @@ TEST_F(FeedTransactionTest, SubBookV1)
|
||||
"close_time_iso": "2000-01-01T00:00:00Z",
|
||||
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
|
||||
})";
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(OrderbookCancelPublish))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
@@ -504,6 +525,7 @@ TEST_F(FeedTransactionTest, SubBookV1)
|
||||
metaObj = CreateMetaDataForCreateOffer(CURRENCY, ISSUER, 22, 3, 1);
|
||||
trans1.metadata = metaObj.getSerializer().peekData();
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(OrderbookCreatePublish))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
@@ -517,7 +539,8 @@ TEST_F(FeedTransactionTest, SubBookV2)
|
||||
{
|
||||
auto const issue1 = GetIssue(CURRENCY, ISSUER);
|
||||
ripple::Book const book{ripple::xrpIssue(), issue1};
|
||||
sessionPtr->apiSubVersion = 2;
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(book, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->bookSubCount(), 1);
|
||||
|
||||
@@ -590,6 +613,7 @@ TEST_F(FeedTransactionTest, SubBookV2)
|
||||
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
|
||||
})";
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(OrderbookPublish))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
@@ -601,11 +625,13 @@ TEST_F(FeedTransactionTest, SubBookV2)
|
||||
|
||||
TEST_F(FeedTransactionTest, TransactionContainsBothAccountsSubed)
|
||||
{
|
||||
sessionPtr->apiSubVersion = 2;
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(account, sessionPtr);
|
||||
|
||||
auto const account2 = GetAccountIDWithString(ACCOUNT2);
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(account2, sessionPtr);
|
||||
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
|
||||
@@ -617,12 +643,14 @@ TEST_F(FeedTransactionTest, TransactionContainsBothAccountsSubed)
|
||||
trans1.ledgerSequence = 32;
|
||||
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
testFeedPtr->unsub(account, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
@@ -634,10 +662,13 @@ TEST_F(FeedTransactionTest, TransactionContainsBothAccountsSubed)
|
||||
TEST_F(FeedTransactionTest, SubAccountRepeatWithDifferentVersion)
|
||||
{
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(account, sessionPtr);
|
||||
|
||||
auto const account2 = GetAccountIDWithString(ACCOUNT2);
|
||||
sessionPtr->apiSubVersion = 2;
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(account2, sessionPtr);
|
||||
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
|
||||
@@ -649,12 +680,14 @@ TEST_F(FeedTransactionTest, SubAccountRepeatWithDifferentVersion)
|
||||
trans1.ledgerSequence = 32;
|
||||
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
testFeedPtr->unsub(account, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
@@ -667,10 +700,10 @@ TEST_F(FeedTransactionTest, SubAccountRepeatWithDifferentVersion)
|
||||
TEST_F(FeedTransactionTest, SubTransactionRepeatWithDifferentVersion)
|
||||
{
|
||||
// sub version 1 first
|
||||
sessionPtr->apiSubVersion = 1;
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
|
||||
// sub version 2 later
|
||||
sessionPtr->apiSubVersion = 2;
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubCount(), 1);
|
||||
|
||||
@@ -681,6 +714,7 @@ TEST_F(FeedTransactionTest, SubTransactionRepeatWithDifferentVersion)
|
||||
trans1.ledgerSequence = 32;
|
||||
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
@@ -693,9 +727,11 @@ TEST_F(FeedTransactionTest, SubTransactionRepeatWithDifferentVersion)
|
||||
TEST_F(FeedTransactionTest, SubRepeat)
|
||||
{
|
||||
auto const session2 = std::make_shared<MockSession>();
|
||||
session2->apiSubVersion = 1;
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
|
||||
EXPECT_CALL(*session2, onDisconnect);
|
||||
testFeedPtr->sub(session2);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubCount(), 2);
|
||||
|
||||
@@ -712,7 +748,11 @@ TEST_F(FeedTransactionTest, SubRepeat)
|
||||
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
auto const account2 = GetAccountIDWithString(ACCOUNT2);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(account, sessionPtr);
|
||||
|
||||
EXPECT_CALL(*session2, onDisconnect);
|
||||
testFeedPtr->sub(account2, session2);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
|
||||
|
||||
@@ -729,8 +769,12 @@ TEST_F(FeedTransactionTest, SubRepeat)
|
||||
|
||||
auto const issue1 = GetIssue(CURRENCY, ISSUER);
|
||||
ripple::Book const book{ripple::xrpIssue(), issue1};
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(book, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->bookSubCount(), 1);
|
||||
|
||||
EXPECT_CALL(*session2, onDisconnect);
|
||||
testFeedPtr->sub(book, session2);
|
||||
EXPECT_EQ(testFeedPtr->bookSubCount(), 2);
|
||||
|
||||
@@ -744,6 +788,7 @@ TEST_F(FeedTransactionTest, SubRepeat)
|
||||
|
||||
TEST_F(FeedTransactionTest, PubTransactionWithOwnerFund)
|
||||
{
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
|
||||
auto const ledgerHeader = CreateLedgerHeader(LEDGERHASH, 33);
|
||||
@@ -814,6 +859,7 @@ TEST_F(FeedTransactionTest, PubTransactionWithOwnerFund)
|
||||
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
|
||||
})";
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TransactionForOwnerFund))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
}
|
||||
@@ -856,6 +902,7 @@ constexpr static auto TRAN_FROZEN =
|
||||
|
||||
TEST_F(FeedTransactionTest, PubTransactionOfferCreationFrozenLine)
|
||||
{
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
|
||||
auto const ledgerHeader = CreateLedgerHeader(LEDGERHASH, 33);
|
||||
@@ -888,12 +935,14 @@ TEST_F(FeedTransactionTest, PubTransactionOfferCreationFrozenLine)
|
||||
ON_CALL(*backend, doFetchLedgerObject(kk, testing::_, testing::_))
|
||||
.WillByDefault(testing::Return(accountRoot.getSerializer().peekData()));
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_FROZEN))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
}
|
||||
|
||||
TEST_F(FeedTransactionTest, SubTransactionOfferCreationGlobalFrozen)
|
||||
{
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
|
||||
auto const ledgerHeader = CreateLedgerHeader(LEDGERHASH, 33);
|
||||
@@ -926,6 +975,7 @@ TEST_F(FeedTransactionTest, SubTransactionOfferCreationGlobalFrozen)
|
||||
ON_CALL(*backend, doFetchLedgerObject(kk, testing::_, testing::_))
|
||||
.WillByDefault(testing::Return(accountRoot.getSerializer().peekData()));
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_FROZEN))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
}
|
||||
@@ -933,7 +983,11 @@ TEST_F(FeedTransactionTest, SubTransactionOfferCreationGlobalFrozen)
|
||||
TEST_F(FeedTransactionTest, SubBothProposedAndValidatedAccount)
|
||||
{
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(account, sessionPtr);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->subProposed(account, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
|
||||
|
||||
@@ -943,6 +997,8 @@ TEST_F(FeedTransactionTest, SubBothProposedAndValidatedAccount)
|
||||
trans1.transaction = obj.getSerializer().peekData();
|
||||
trans1.ledgerSequence = 32;
|
||||
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
@@ -955,7 +1011,10 @@ TEST_F(FeedTransactionTest, SubBothProposedAndValidatedAccount)
|
||||
|
||||
TEST_F(FeedTransactionTest, SubBothProposedAndValidated)
|
||||
{
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->subProposed(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubCount(), 1);
|
||||
|
||||
@@ -966,6 +1025,7 @@ TEST_F(FeedTransactionTest, SubBothProposedAndValidated)
|
||||
trans1.ledgerSequence = 32;
|
||||
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).Times(2).WillRepeatedly(testing::Return(1));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(2);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
@@ -976,6 +1036,7 @@ TEST_F(FeedTransactionTest, SubBothProposedAndValidated)
|
||||
|
||||
TEST_F(FeedTransactionTest, SubProposedDisconnect)
|
||||
{
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->subProposed(sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->transactionSubCount(), 0);
|
||||
|
||||
@@ -986,6 +1047,7 @@ TEST_F(FeedTransactionTest, SubProposedDisconnect)
|
||||
trans1.ledgerSequence = 32;
|
||||
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
@@ -996,6 +1058,8 @@ TEST_F(FeedTransactionTest, SubProposedDisconnect)
|
||||
TEST_F(FeedTransactionTest, SubProposedAccountDisconnect)
|
||||
{
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->subProposed(account, sessionPtr);
|
||||
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
|
||||
|
||||
@@ -1006,6 +1070,7 @@ TEST_F(FeedTransactionTest, SubProposedAccountDisconnect)
|
||||
trans1.ledgerSequence = 32;
|
||||
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
|
||||
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
|
||||
testFeedPtr->pub(trans1, ledgerHeader, backend);
|
||||
|
||||
@@ -1015,21 +1080,9 @@ TEST_F(FeedTransactionTest, SubProposedAccountDisconnect)
|
||||
|
||||
struct TransactionFeedMockPrometheusTest : WithMockPrometheus, SyncExecutionCtxFixture {
|
||||
protected:
|
||||
std::shared_ptr<web::ConnectionBase> sessionPtr;
|
||||
std::shared_ptr<TransactionFeed> testFeedPtr;
|
||||
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
testFeedPtr = std::make_shared<TransactionFeed>(ctx);
|
||||
sessionPtr = std::make_shared<MockSession>();
|
||||
}
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
sessionPtr.reset();
|
||||
testFeedPtr.reset();
|
||||
}
|
||||
web::SubscriptionContextPtr sessionPtr = std::make_shared<MockSession>();
|
||||
std::shared_ptr<TransactionFeed> testFeedPtr = std::make_shared<TransactionFeed>(ctx);
|
||||
MockSession* mockSessionPtr = dynamic_cast<MockSession*>(sessionPtr.get());
|
||||
};
|
||||
|
||||
TEST_F(TransactionFeedMockPrometheusTest, subUnsub)
|
||||
@@ -1045,15 +1098,18 @@ TEST_F(TransactionFeedMockPrometheusTest, subUnsub)
|
||||
EXPECT_CALL(counterBook, add(1));
|
||||
EXPECT_CALL(counterBook, add(-1));
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
testFeedPtr->unsub(sessionPtr);
|
||||
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(account, sessionPtr);
|
||||
testFeedPtr->unsub(account, sessionPtr);
|
||||
|
||||
auto const issue1 = GetIssue(CURRENCY, ISSUER);
|
||||
ripple::Book const book{ripple::xrpIssue(), issue1};
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect);
|
||||
testFeedPtr->sub(book, sessionPtr);
|
||||
testFeedPtr->unsub(book, sessionPtr);
|
||||
}
|
||||
@@ -1071,6 +1127,11 @@ TEST_F(TransactionFeedMockPrometheusTest, AutoDisconnect)
|
||||
EXPECT_CALL(counterBook, add(1));
|
||||
EXPECT_CALL(counterBook, add(-1));
|
||||
|
||||
std::vector<web::SubscriptionContextInterface::OnDisconnectSlot> onDisconnectSlots;
|
||||
|
||||
EXPECT_CALL(*mockSessionPtr, onDisconnect).Times(3).WillRepeatedly([&onDisconnectSlots](auto const& slot) {
|
||||
onDisconnectSlots.push_back(slot);
|
||||
});
|
||||
testFeedPtr->sub(sessionPtr);
|
||||
|
||||
auto const account = GetAccountIDWithString(ACCOUNT1);
|
||||
@@ -1080,5 +1141,9 @@ TEST_F(TransactionFeedMockPrometheusTest, AutoDisconnect)
|
||||
ripple::Book const book{ripple::xrpIssue(), issue1};
|
||||
testFeedPtr->sub(book, sessionPtr);
|
||||
|
||||
// Emulate onDisconnect signal is called
|
||||
for (auto const& slot : onDisconnectSlots)
|
||||
slot(sessionPtr.get());
|
||||
|
||||
sessionPtr.reset();
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
#include "util/MockWsBase.hpp"
|
||||
#include "util/NameGenerator.hpp"
|
||||
#include "util/TestObject.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
@@ -63,21 +63,9 @@ constexpr static auto PAYS20XRPGETS10USDBOOKDIR = "7B1767D41DBCE79D9585CF9D0262A
|
||||
constexpr static auto INDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
|
||||
constexpr static auto INDEX2 = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
|
||||
|
||||
class RPCSubscribeHandlerTest : public HandlerBaseTest {
|
||||
protected:
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
HandlerBaseTest::SetUp();
|
||||
session_ = std::make_shared<MockSession>();
|
||||
}
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
HandlerBaseTest::TearDown();
|
||||
}
|
||||
|
||||
std::shared_ptr<web::ConnectionBase> session_;
|
||||
struct RPCSubscribeHandlerTest : HandlerBaseTest {
|
||||
web::SubscriptionContextPtr session_ = std::make_shared<MockSession>();
|
||||
MockSession* mockSession_ = dynamic_cast<MockSession*>(session_.get());
|
||||
StrictMockSubscriptionManagerSharedPtr mockSubscriptionManagerPtr;
|
||||
};
|
||||
|
||||
@@ -578,6 +566,7 @@ TEST_F(RPCSubscribeHandlerTest, EmptyResponse)
|
||||
{
|
||||
runSpawn([&, this](auto yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
|
||||
EXPECT_CALL(*mockSession_, setApiSubversion(0));
|
||||
auto const output = handler.process(json::parse(R"({})"), Context{yield, session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_TRUE(output.result->as_object().empty());
|
||||
@@ -594,12 +583,13 @@ TEST_F(RPCSubscribeHandlerTest, StreamsWithoutLedger)
|
||||
);
|
||||
runSpawn([&, this](auto yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subTransactions).Times(1);
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subValidation).Times(1);
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subManifest).Times(1);
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subBookChanges).Times(1);
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subProposedTransactions).Times(1);
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subTransactions);
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subValidation);
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subManifest);
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subBookChanges);
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subProposedTransactions);
|
||||
|
||||
EXPECT_CALL(*mockSession_, setApiSubversion(0));
|
||||
auto const output = handler.process(input, Context{yield, session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_TRUE(output.result->as_object().empty());
|
||||
@@ -630,6 +620,7 @@ TEST_F(RPCSubscribeHandlerTest, StreamsLedger)
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subLedger)
|
||||
.WillOnce(testing::Return(boost::json::parse(expectedOutput).as_object()));
|
||||
|
||||
EXPECT_CALL(*mockSession_, setApiSubversion(0));
|
||||
auto const output = handler.process(input, Context{yield, session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(output.result->as_object(), json::parse(expectedOutput));
|
||||
@@ -649,8 +640,9 @@ TEST_F(RPCSubscribeHandlerTest, Accounts)
|
||||
runSpawn([&, this](auto yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
|
||||
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subAccount(GetAccountIDWithString(ACCOUNT), session_)).Times(1);
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subAccount(GetAccountIDWithString(ACCOUNT), session_));
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subAccount(GetAccountIDWithString(ACCOUNT2), session_)).Times(2);
|
||||
EXPECT_CALL(*mockSession_, setApiSubversion(0));
|
||||
auto const output = handler.process(input, Context{yield, session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_TRUE(output.result->as_object().empty());
|
||||
@@ -670,10 +662,10 @@ TEST_F(RPCSubscribeHandlerTest, AccountsProposed)
|
||||
runSpawn([&, this](auto yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
|
||||
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subProposedAccount(GetAccountIDWithString(ACCOUNT), session_))
|
||||
.Times(1);
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subProposedAccount(GetAccountIDWithString(ACCOUNT), session_));
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subProposedAccount(GetAccountIDWithString(ACCOUNT2), session_))
|
||||
.Times(2);
|
||||
EXPECT_CALL(*mockSession_, setApiSubversion(0));
|
||||
auto const output = handler.process(input, Context{yield, session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_TRUE(output.result->as_object().empty());
|
||||
@@ -703,7 +695,8 @@ TEST_F(RPCSubscribeHandlerTest, JustBooks)
|
||||
));
|
||||
runSpawn([&, this](auto yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subBook).Times(1);
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subBook);
|
||||
EXPECT_CALL(*mockSession_, setApiSubversion(0));
|
||||
auto const output = handler.process(input, Context{yield, session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_TRUE(output.result->as_object().empty());
|
||||
@@ -735,6 +728,7 @@ TEST_F(RPCSubscribeHandlerTest, BooksBothSet)
|
||||
runSpawn([&, this](auto yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subBook).Times(2);
|
||||
EXPECT_CALL(*mockSession_, setApiSubversion(0));
|
||||
auto const output = handler.process(input, Context{yield, session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_TRUE(output.result->as_object().empty());
|
||||
@@ -899,6 +893,7 @@ TEST_F(RPCSubscribeHandlerTest, BooksBothSnapshotSet)
|
||||
runSpawn([&, this](auto yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subBook).Times(2);
|
||||
EXPECT_CALL(*mockSession_, setApiSubversion(0));
|
||||
auto const output = handler.process(input, Context{yield, session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(output.result->as_object().at("bids").as_array().size(), 10);
|
||||
@@ -1007,7 +1002,7 @@ TEST_F(RPCSubscribeHandlerTest, BooksBothUnsetSnapshotSet)
|
||||
std::vector<Blob> const bbs2(10, gets10USDPays20XRPOffer.getSerializer().peekData());
|
||||
ON_CALL(*backend, doFetchLedgerObjects(indexes2, MAXSEQ, _)).WillByDefault(Return(bbs2));
|
||||
|
||||
EXPECT_CALL(*backend, doFetchLedgerObjects).Times(1);
|
||||
EXPECT_CALL(*backend, doFetchLedgerObjects);
|
||||
|
||||
static auto const expectedOffer = fmt::format(
|
||||
R"({{
|
||||
@@ -1038,7 +1033,8 @@ TEST_F(RPCSubscribeHandlerTest, BooksBothUnsetSnapshotSet)
|
||||
|
||||
runSpawn([&, this](auto yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subBook).Times(1);
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subBook);
|
||||
EXPECT_CALL(*mockSession_, setApiSubversion(0));
|
||||
auto const output = handler.process(input, Context{yield, session_});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(output.result->as_object().at("offers").as_array().size(), 10);
|
||||
@@ -1056,11 +1052,12 @@ TEST_F(RPCSubscribeHandlerTest, APIVersion)
|
||||
auto const apiVersion = 2;
|
||||
runSpawn([&, this](auto yield) {
|
||||
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subProposedTransactions).Times(1);
|
||||
EXPECT_CALL(*mockSubscriptionManagerPtr, subProposedTransactions);
|
||||
EXPECT_CALL(*mockSession_, setApiSubversion(apiVersion));
|
||||
auto const output =
|
||||
handler.process(input, Context{.yield = yield, .session = session_, .apiVersion = apiVersion});
|
||||
ASSERT_TRUE(output);
|
||||
EXPECT_EQ(session_->apiSubVersion, apiVersion);
|
||||
// EXPECT_EQ(session_->apiSubVersion, apiVersion);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
#include "util/MockSubscriptionManager.hpp"
|
||||
#include "util/MockWsBase.hpp"
|
||||
#include "util/NameGenerator.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
@@ -49,19 +49,7 @@ constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
|
||||
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
|
||||
|
||||
struct RPCUnsubscribeTest : HandlerBaseTest {
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
HandlerBaseTest::SetUp();
|
||||
session_ = std::make_shared<MockSession>();
|
||||
}
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
HandlerBaseTest::TearDown();
|
||||
}
|
||||
|
||||
std::shared_ptr<web::ConnectionBase> session_;
|
||||
web::SubscriptionContextPtr session_ = std::make_shared<MockSession>();
|
||||
StrictMockSubscriptionManagerSharedPtr mockSubscriptionManagerPtr;
|
||||
};
|
||||
|
||||
|
||||
@@ -155,10 +155,12 @@ TEST_F(CoroutineGroupTests, TooManyCoroutines)
|
||||
}));
|
||||
|
||||
EXPECT_FALSE(group.spawn(yield, [this](boost::asio::yield_context) { callback2_.Call(); }));
|
||||
EXPECT_TRUE(group.isFull());
|
||||
|
||||
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{2}};
|
||||
timer.async_wait(yield);
|
||||
|
||||
EXPECT_FALSE(group.isFull());
|
||||
EXPECT_TRUE(group.spawn(yield, [this](boost::asio::yield_context) { callback2_.Call(); }));
|
||||
|
||||
group.asyncWait(yield);
|
||||
@@ -166,16 +168,28 @@ TEST_F(CoroutineGroupTests, TooManyCoroutines)
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(CoroutineGroupTests, CanSpawn)
|
||||
TEST_F(CoroutineGroupTests, SpawnForeign)
|
||||
{
|
||||
EXPECT_CALL(callback1_, Call);
|
||||
testing::Sequence const sequence;
|
||||
EXPECT_CALL(callback1_, Call).InSequence(sequence);
|
||||
EXPECT_CALL(callback2_, Call).InSequence(sequence);
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
CoroutineGroup group{yield, 1};
|
||||
EXPECT_TRUE(group.canSpawn());
|
||||
group.spawn(yield, [&group, this](boost::asio::yield_context) {
|
||||
|
||||
auto const onForeignComplete = group.registerForeign();
|
||||
[&]() { ASSERT_TRUE(onForeignComplete.has_value()); }();
|
||||
|
||||
[&]() { ASSERT_FALSE(group.registerForeign().has_value()); }();
|
||||
|
||||
boost::asio::spawn(ctx, [this, &onForeignComplete](boost::asio::yield_context innerYield) {
|
||||
boost::asio::steady_timer timer{innerYield.get_executor(), std::chrono::milliseconds{2}};
|
||||
timer.async_wait(innerYield);
|
||||
callback1_.Call();
|
||||
EXPECT_FALSE(group.canSpawn());
|
||||
onForeignComplete->operator()();
|
||||
});
|
||||
|
||||
group.asyncWait(yield);
|
||||
callback2_.Call();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
//==============================================================================
|
||||
|
||||
#include "util/LoggerFixtures.hpp"
|
||||
#include "util/NameGenerator.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
#include "web/impl/AdminVerificationStrategy.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
@@ -34,7 +35,7 @@ namespace http = boost::beast::http;
|
||||
|
||||
class IPAdminVerificationStrategyTest : public NoLoggerFixture {
|
||||
protected:
|
||||
web::impl::IPAdminVerificationStrategy strat_;
|
||||
web::IPAdminVerificationStrategy strat_;
|
||||
http::request<http::string_body> request_;
|
||||
};
|
||||
|
||||
@@ -52,7 +53,7 @@ protected:
|
||||
std::string const password_ = "secret";
|
||||
std::string const passwordHash_ = "2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b";
|
||||
|
||||
web::impl::PasswordAdminVerificationStrategy strat_{password_};
|
||||
web::PasswordAdminVerificationStrategy strat_{password_};
|
||||
|
||||
static http::request<http::string_body>
|
||||
makeRequest(std::string const& password, http::field const field = http::field::authorization)
|
||||
@@ -92,10 +93,10 @@ class MakeAdminVerificationStrategyTest : public testing::TestWithParam<MakeAdmi
|
||||
|
||||
TEST_P(MakeAdminVerificationStrategyTest, ChoosesStrategyCorrectly)
|
||||
{
|
||||
auto strat = web::impl::make_AdminVerificationStrategy(GetParam().passwordOpt);
|
||||
auto ipStrat = dynamic_cast<web::impl::IPAdminVerificationStrategy*>(strat.get());
|
||||
auto strat = web::make_AdminVerificationStrategy(GetParam().passwordOpt);
|
||||
auto ipStrat = dynamic_cast<web::IPAdminVerificationStrategy*>(strat.get());
|
||||
EXPECT_EQ(ipStrat != nullptr, GetParam().expectIpStrategy);
|
||||
auto passwordStrat = dynamic_cast<web::impl::PasswordAdminVerificationStrategy*>(strat.get());
|
||||
auto passwordStrat = dynamic_cast<web::PasswordAdminVerificationStrategy*>(strat.get());
|
||||
EXPECT_EQ(passwordStrat != nullptr, GetParam().expectPasswordStrategy);
|
||||
}
|
||||
|
||||
@@ -136,10 +137,8 @@ struct MakeAdminVerificationStrategyFromConfigTest
|
||||
TEST_P(MakeAdminVerificationStrategyFromConfigTest, ChecksConfig)
|
||||
{
|
||||
util::Config const serverConfig{boost::json::parse(GetParam().config)};
|
||||
auto const result = web::impl::make_AdminVerificationStrategy(serverConfig);
|
||||
if (GetParam().expectedError) {
|
||||
EXPECT_FALSE(result.has_value());
|
||||
}
|
||||
auto const result = web::make_AdminVerificationStrategy(serverConfig);
|
||||
EXPECT_EQ(not result.has_value(), GetParam().expectedError);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
@@ -149,32 +148,33 @@ INSTANTIATE_TEST_SUITE_P(
|
||||
MakeAdminVerificationStrategyFromConfigTestParams{
|
||||
.testName = "NoPasswordNoLocalAdmin",
|
||||
.config = "{}",
|
||||
.expectedError = true
|
||||
.expectedError = false
|
||||
},
|
||||
MakeAdminVerificationStrategyFromConfigTestParams{
|
||||
.testName = "OnlyPassword",
|
||||
.config = R"({"admin_password": "password"})",
|
||||
.config = R"({"server": {"admin_password": "password"}})",
|
||||
.expectedError = false
|
||||
},
|
||||
MakeAdminVerificationStrategyFromConfigTestParams{
|
||||
.testName = "OnlyLocalAdmin",
|
||||
.config = R"({"local_admin": true})",
|
||||
.config = R"({"server": {"local_admin": true}})",
|
||||
.expectedError = false
|
||||
},
|
||||
MakeAdminVerificationStrategyFromConfigTestParams{
|
||||
.testName = "OnlyLocalAdminDisabled",
|
||||
.config = R"({"local_admin": false})",
|
||||
.config = R"({"server": {"local_admin": false}})",
|
||||
.expectedError = true
|
||||
},
|
||||
MakeAdminVerificationStrategyFromConfigTestParams{
|
||||
.testName = "LocalAdminAndPassword",
|
||||
.config = R"({"local_admin": true, "admin_password": "password"})",
|
||||
.config = R"({"server": {"local_admin": true, "admin_password": "password"}})",
|
||||
.expectedError = true
|
||||
},
|
||||
MakeAdminVerificationStrategyFromConfigTestParams{
|
||||
.testName = "LocalAdminDisabledAndPassword",
|
||||
.config = R"({"local_admin": false, "admin_password": "password"})",
|
||||
.config = R"({"server": {"local_admin": false, "admin_password": "password"}})",
|
||||
.expectedError = false
|
||||
}
|
||||
)
|
||||
),
|
||||
tests::util::NameGenerator
|
||||
);
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
#include "web/RPCServerHandler.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/beast/http/status.hpp>
|
||||
@@ -61,28 +62,25 @@ struct MockWsBase : public web::ConnectionBase {
|
||||
lastStatus = status;
|
||||
}
|
||||
|
||||
SubscriptionContextPtr
|
||||
makeSubscriptionContext(util::TagDecoratorFactory const&) override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
MockWsBase(util::TagDecoratorFactory const& factory) : web::ConnectionBase(factory, "localhost.fake.ip")
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct WebRPCServerHandlerTest : util::prometheus::WithPrometheus, MockBackendTest, SyncAsioContextTest {
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
etl = std::make_shared<MockETLService>();
|
||||
rpcEngine = std::make_shared<MockAsyncRPCEngine>();
|
||||
tagFactory = std::make_shared<util::TagDecoratorFactory>(cfg);
|
||||
session = std::make_shared<MockWsBase>(*tagFactory);
|
||||
handler = std::make_shared<RPCServerHandler<MockAsyncRPCEngine, MockETLService>>(cfg, backend, rpcEngine, etl);
|
||||
}
|
||||
|
||||
std::shared_ptr<MockAsyncRPCEngine> rpcEngine;
|
||||
std::shared_ptr<MockETLService> etl;
|
||||
std::shared_ptr<util::TagDecoratorFactory> tagFactory;
|
||||
std::shared_ptr<RPCServerHandler<MockAsyncRPCEngine, MockETLService>> handler;
|
||||
std::shared_ptr<MockWsBase> session;
|
||||
util::Config cfg;
|
||||
std::shared_ptr<MockAsyncRPCEngine> rpcEngine = std::make_shared<MockAsyncRPCEngine>();
|
||||
std::shared_ptr<MockETLService> etl = std::make_shared<MockETLService>();
|
||||
std::shared_ptr<util::TagDecoratorFactory> tagFactory = std::make_shared<util::TagDecoratorFactory>(cfg);
|
||||
std::shared_ptr<RPCServerHandler<MockAsyncRPCEngine, MockETLService>> handler =
|
||||
std::make_shared<RPCServerHandler<MockAsyncRPCEngine, MockETLService>>(cfg, backend, rpcEngine, etl);
|
||||
std::shared_ptr<MockWsBase> session = std::make_shared<MockWsBase>(*tagFactory);
|
||||
};
|
||||
|
||||
TEST_F(WebRPCServerHandlerTest, HTTPDefaultPath)
|
||||
@@ -524,7 +522,7 @@ TEST_F(WebRPCServerHandlerTest, HTTPBadSyntaxWhenRequestSubscribe)
|
||||
"result": {
|
||||
"error": "badSyntax",
|
||||
"error_code": 1,
|
||||
"error_message": "Subscribe and unsubscribe are only allowed or websocket.",
|
||||
"error_message": "Subscribe and unsubscribe are only allowed for websocket.",
|
||||
"status": "error",
|
||||
"type": "response",
|
||||
"request": {
|
||||
|
||||
@@ -26,12 +26,12 @@
|
||||
#include "util/config/Config.hpp"
|
||||
#include "util/prometheus/Label.hpp"
|
||||
#include "util/prometheus/Prometheus.hpp"
|
||||
#include "web/AdminVerificationStrategy.hpp"
|
||||
#include "web/Server.hpp"
|
||||
#include "web/dosguard/DOSGuard.hpp"
|
||||
#include "web/dosguard/DOSGuardInterface.hpp"
|
||||
#include "web/dosguard/IntervalSweepHandler.hpp"
|
||||
#include "web/dosguard/WhitelistHandler.hpp"
|
||||
#include "web/impl/AdminVerificationStrategy.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
72
tests/unit/web/SubscriptionContextTests.cpp
Normal file
72
tests/unit/web/SubscriptionContextTests.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/LoggerFixtures.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
#include "web/SubscriptionContext.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/interface/ConnectionBaseMock.hpp"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
using namespace web;
|
||||
|
||||
struct SubscriptionContextTests : NoLoggerFixture {
|
||||
util::TagDecoratorFactory tagFactory_{util::Config{}};
|
||||
ConnectionBaseStrictMockPtr connection_ =
|
||||
std::make_shared<testing::StrictMock<ConnectionBaseMock>>(tagFactory_, "some ip");
|
||||
|
||||
SubscriptionContext subscriptionContext_{tagFactory_, connection_};
|
||||
testing::StrictMock<testing::MockFunction<void(SubscriptionContextInterface*)>> callbackMock_;
|
||||
};
|
||||
|
||||
TEST_F(SubscriptionContextTests, send)
|
||||
{
|
||||
auto message = std::make_shared<std::string>("message");
|
||||
EXPECT_CALL(*connection_, send(message));
|
||||
subscriptionContext_.send(message);
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionContextTests, sendConnectionExpired)
|
||||
{
|
||||
auto message = std::make_shared<std::string>("message");
|
||||
connection_.reset();
|
||||
subscriptionContext_.send(message);
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionContextTests, onDisconnect)
|
||||
{
|
||||
auto localContext = std::make_unique<SubscriptionContext>(tagFactory_, connection_);
|
||||
localContext->onDisconnect(callbackMock_.AsStdFunction());
|
||||
|
||||
EXPECT_CALL(callbackMock_, Call(localContext.get()));
|
||||
localContext.reset();
|
||||
}
|
||||
|
||||
TEST_F(SubscriptionContextTests, setApiSubversion)
|
||||
{
|
||||
EXPECT_EQ(subscriptionContext_.apiSubversion(), 0);
|
||||
subscriptionContext_.setApiSubversion(42);
|
||||
EXPECT_EQ(subscriptionContext_.apiSubversion(), 42);
|
||||
}
|
||||
288
tests/unit/web/impl/ErrorHandlingTests.cpp
Normal file
288
tests/unit/web/impl/ErrorHandlingTests.cpp
Normal file
@@ -0,0 +1,288 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 "util/Taggable.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/impl/ErrorHandling.hpp"
|
||||
#include "web/interface/ConnectionBase.hpp"
|
||||
#include "web/interface/ConnectionBaseMock.hpp"
|
||||
|
||||
#include <boost/beast/http/status.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
using namespace web::impl;
|
||||
using namespace web;
|
||||
|
||||
struct ErrorHandlingTests : NoLoggerFixture {
|
||||
util::TagDecoratorFactory tagFactory_{util::Config{}};
|
||||
std::string const clientIp_ = "some ip";
|
||||
ConnectionBaseStrictMockPtr connection_ =
|
||||
std::make_shared<testing::StrictMock<ConnectionBaseMock>>(tagFactory_, clientIp_);
|
||||
};
|
||||
|
||||
struct ErrorHandlingComposeErrorTestBundle {
|
||||
std::string testName;
|
||||
bool connectionUpgraded;
|
||||
std::optional<boost::json::object> request;
|
||||
boost::json::object expectedResult;
|
||||
};
|
||||
|
||||
struct ErrorHandlingComposeErrorTest : ErrorHandlingTests,
|
||||
testing::WithParamInterface<ErrorHandlingComposeErrorTestBundle> {};
|
||||
|
||||
TEST_P(ErrorHandlingComposeErrorTest, composeError)
|
||||
{
|
||||
connection_->upgraded = GetParam().connectionUpgraded;
|
||||
ErrorHelper errorHelper{connection_, GetParam().request};
|
||||
auto const result = errorHelper.composeError(rpc::RippledError::rpcNOT_READY);
|
||||
EXPECT_EQ(boost::json::serialize(result), boost::json::serialize(GetParam().expectedResult));
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
ErrorHandlingComposeErrorTestGroup,
|
||||
ErrorHandlingComposeErrorTest,
|
||||
testing::ValuesIn(
|
||||
{ErrorHandlingComposeErrorTestBundle{
|
||||
"NoRequest_UpgradedConnection",
|
||||
true,
|
||||
std::nullopt,
|
||||
{{"error", "notReady"},
|
||||
{"error_code", 13},
|
||||
{"error_message", "Not ready to handle this request."},
|
||||
{"status", "error"},
|
||||
{"type", "response"}}
|
||||
},
|
||||
ErrorHandlingComposeErrorTestBundle{
|
||||
"NoRequest_NotUpgradedConnection",
|
||||
false,
|
||||
std::nullopt,
|
||||
{{"result",
|
||||
{{"error", "notReady"},
|
||||
{"error_code", 13},
|
||||
{"error_message", "Not ready to handle this request."},
|
||||
{"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."},
|
||||
{"status", "error"},
|
||||
{"type", "response"},
|
||||
{"id", 1},
|
||||
{"api_version", 2},
|
||||
{"request", {{"id", 1}, {"api_version", 2}}}}
|
||||
},
|
||||
ErrorHandlingComposeErrorTestBundle{
|
||||
"Request_NotUpgradedConnection",
|
||||
false,
|
||||
boost::json::object{{"id", 1}, {"api_version", 2}},
|
||||
{{"result",
|
||||
{{"error", "notReady"},
|
||||
{"error_code", 13},
|
||||
{"error_message", "Not ready to handle this request."},
|
||||
{"status", "error"},
|
||||
{"type", "response"},
|
||||
{"id", 1},
|
||||
{"request", {{"id", 1}, {"api_version", 2}}}}}}
|
||||
}}
|
||||
),
|
||||
tests::util::NameGenerator
|
||||
);
|
||||
|
||||
struct ErrorHandlingSendErrorTestBundle {
|
||||
std::string testName;
|
||||
bool connectionUpgraded;
|
||||
rpc::Status status;
|
||||
std::string expectedMessage;
|
||||
boost::beast::http::status expectedStatus;
|
||||
};
|
||||
|
||||
struct ErrorHandlingSendErrorTest : ErrorHandlingTests,
|
||||
testing::WithParamInterface<ErrorHandlingSendErrorTestBundle> {};
|
||||
|
||||
TEST_P(ErrorHandlingSendErrorTest, sendError)
|
||||
{
|
||||
connection_->upgraded = GetParam().connectionUpgraded;
|
||||
ErrorHelper errorHelper{connection_};
|
||||
|
||||
EXPECT_CALL(*connection_, send(std::string{GetParam().expectedMessage}, GetParam().expectedStatus));
|
||||
errorHelper.sendError(GetParam().status);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
ErrorHandlingSendErrorTestGroup,
|
||||
ErrorHandlingSendErrorTest,
|
||||
testing::ValuesIn({
|
||||
ErrorHandlingSendErrorTestBundle{
|
||||
"UpgradedConnection",
|
||||
true,
|
||||
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
|
||||
R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})",
|
||||
boost::beast::http::status::ok
|
||||
},
|
||||
ErrorHandlingSendErrorTestBundle{
|
||||
"NotUpgradedConnection_InvalidApiVersion",
|
||||
false,
|
||||
rpc::Status{rpc::ClioError::rpcINVALID_API_VERSION},
|
||||
"invalid_API_version",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingSendErrorTestBundle{
|
||||
"NotUpgradedConnection_CommandIsMissing",
|
||||
false,
|
||||
rpc::Status{rpc::ClioError::rpcCOMMAND_IS_MISSING},
|
||||
"Null method",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingSendErrorTestBundle{
|
||||
"NotUpgradedConnection_CommandIsEmpty",
|
||||
false,
|
||||
rpc::Status{rpc::ClioError::rpcCOMMAND_IS_EMPTY},
|
||||
"method is empty",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingSendErrorTestBundle{
|
||||
"NotUpgradedConnection_CommandNotString",
|
||||
false,
|
||||
rpc::Status{rpc::ClioError::rpcCOMMAND_NOT_STRING},
|
||||
"method is not string",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingSendErrorTestBundle{
|
||||
"NotUpgradedConnection_ParamsUnparseable",
|
||||
false,
|
||||
rpc::Status{rpc::ClioError::rpcPARAMS_UNPARSEABLE},
|
||||
"params unparseable",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ErrorHandlingSendErrorTestBundle{
|
||||
"NotUpgradedConnection_RippledError",
|
||||
false,
|
||||
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
|
||||
R"({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
}),
|
||||
tests::util::NameGenerator
|
||||
);
|
||||
|
||||
TEST_F(ErrorHandlingTests, sendInternalError)
|
||||
{
|
||||
ErrorHelper errorHelper{connection_};
|
||||
|
||||
EXPECT_CALL(
|
||||
*connection_,
|
||||
send(
|
||||
std::string{
|
||||
R"({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"}})"
|
||||
},
|
||||
boost::beast::http::status::internal_server_error
|
||||
)
|
||||
);
|
||||
errorHelper.sendInternalError();
|
||||
}
|
||||
|
||||
TEST_F(ErrorHandlingTests, sendNotReadyError)
|
||||
{
|
||||
ErrorHelper errorHelper{connection_};
|
||||
EXPECT_CALL(
|
||||
*connection_,
|
||||
send(
|
||||
std::string{
|
||||
R"({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})"
|
||||
},
|
||||
boost::beast::http::status::ok
|
||||
)
|
||||
);
|
||||
errorHelper.sendNotReadyError();
|
||||
}
|
||||
|
||||
TEST_F(ErrorHandlingTests, sendTooBusyError_UpgradedConnection)
|
||||
{
|
||||
connection_->upgraded = true;
|
||||
ErrorHelper errorHelper{connection_};
|
||||
EXPECT_CALL(
|
||||
*connection_,
|
||||
send(
|
||||
std::string{
|
||||
R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})"
|
||||
},
|
||||
boost::beast::http::status::ok
|
||||
)
|
||||
);
|
||||
errorHelper.sendTooBusyError();
|
||||
}
|
||||
|
||||
TEST_F(ErrorHandlingTests, sendTooBusyError_NotUpgradedConnection)
|
||||
{
|
||||
connection_->upgraded = false;
|
||||
ErrorHelper errorHelper{connection_};
|
||||
EXPECT_CALL(
|
||||
*connection_,
|
||||
send(
|
||||
std::string{
|
||||
R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})"
|
||||
},
|
||||
boost::beast::http::status::service_unavailable
|
||||
)
|
||||
);
|
||||
errorHelper.sendTooBusyError();
|
||||
}
|
||||
|
||||
TEST_F(ErrorHandlingTests, sendJsonParsingError_UpgradedConnection)
|
||||
{
|
||||
connection_->upgraded = true;
|
||||
ErrorHelper errorHelper{connection_};
|
||||
EXPECT_CALL(
|
||||
*connection_,
|
||||
send(
|
||||
std::string{
|
||||
R"({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})"
|
||||
},
|
||||
boost::beast::http::status::ok
|
||||
)
|
||||
);
|
||||
errorHelper.sendJsonParsingError();
|
||||
}
|
||||
|
||||
TEST_F(ErrorHandlingTests, sendJsonParsingError_NotUpgradedConnection)
|
||||
{
|
||||
connection_->upgraded = false;
|
||||
ErrorHelper errorHelper{connection_};
|
||||
EXPECT_CALL(
|
||||
*connection_,
|
||||
send(std::string{"Unable to parse JSON from the request"}, boost::beast::http::status::bad_request)
|
||||
);
|
||||
errorHelper.sendJsonParsingError();
|
||||
}
|
||||
441
tests/unit/web/ng/RPCServerHandlerTests.cpp
Normal file
441
tests/unit/web/ng/RPCServerHandlerTests.cpp
Normal file
@@ -0,0 +1,441 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/Config.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/MockConnection.hpp"
|
||||
#include "web/ng/RPCServerHandler.hpp"
|
||||
#include "web/ng/Request.hpp"
|
||||
|
||||
#include <boost/asio/spawn.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;
|
||||
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
struct ng_RPCServerHandlerTest : util::prometheus::WithPrometheus, MockBackendTestStrict, SyncAsioContextTest {
|
||||
std::shared_ptr<testing::StrictMock<MockRPCEngine>> rpcEngine_ =
|
||||
std::make_shared<testing::StrictMock<MockRPCEngine>>();
|
||||
std::shared_ptr<StrictMock<MockETLService>> etl_ = std::make_shared<StrictMock<MockETLService>>();
|
||||
RPCServerHandler<MockRPCEngine, MockETLService> rpcServerHandler_{util::Config{}, backend, rpcEngine_, etl_};
|
||||
|
||||
util::TagDecoratorFactory tagFactory_{util::Config{}};
|
||||
StrictMockConnectionMetadata connectionMetadata_{"some ip", tagFactory_};
|
||||
|
||||
static Request
|
||||
makeHttpRequest(std::string_view body)
|
||||
{
|
||||
return Request{http::request<http::string_body>{http::verb::post, "/", 11, body}};
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ng_RPCServerHandlerTest, PostToRpcEngineFailed)
|
||||
{
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest("some message");
|
||||
|
||||
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(ng_RPCServerHandlerTest, 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(*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(ng_RPCServerHandlerTest, JsonParseFailed)
|
||||
{
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest("not a json");
|
||||
|
||||
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(ng_RPCServerHandlerTest, GotNotJsonObject)
|
||||
{
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest("[]");
|
||||
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(ng_RPCServerHandlerTest, HandleRequest_NoRangeFromBackend)
|
||||
{
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest("{}");
|
||||
|
||||
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(ng_RPCServerHandlerTest, HandleRequest_ContextCreationFailed)
|
||||
{
|
||||
backend->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest("{}");
|
||||
|
||||
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(ng_RPCServerHandlerTest, HandleRequest_BuildResponseFailed)
|
||||
{
|
||||
backend->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest(R"json({"method":"some_method"})json");
|
||||
|
||||
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::rpcUNKNOWN_OPTION}}));
|
||||
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::warnRPC_CLIO);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ng_RPCServerHandlerTest, HandleRequest_BuildResponseThrewAnException)
|
||||
{
|
||||
backend->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest(R"json({"method":"some_method"})json");
|
||||
|
||||
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(ng_RPCServerHandlerTest, HandleRequest_Successful_HttpRequest)
|
||||
{
|
||||
backend->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest(R"json({"method":"some_method"})json");
|
||||
|
||||
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::warnRPC_CLIO);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ng_RPCServerHandlerTest, HandleRequest_OutdatedWarning)
|
||||
{
|
||||
backend->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest(R"json({"method":"some_method"})json");
|
||||
|
||||
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::warnRPC_CLIO));
|
||||
EXPECT_TRUE(warningCodes.contains(rpc::warnRPC_OUTDATED));
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ng_RPCServerHandlerTest, HandleRequest_Successful_HttpRequest_Forwarded)
|
||||
{
|
||||
backend->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest(R"json({"method":"some_method"})json");
|
||||
|
||||
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::warnRPC_CLIO);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ng_RPCServerHandlerTest, HandleRequest_Successful_HttpRequest_HasError)
|
||||
{
|
||||
backend->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto const request = makeHttpRequest(R"json({"method":"some_method"})json");
|
||||
|
||||
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::warnRPC_CLIO);
|
||||
});
|
||||
}
|
||||
|
||||
struct ng_RPCServerHandlerWsTest : ng_RPCServerHandlerTest {
|
||||
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>;
|
||||
|
||||
std::shared_ptr<StrictMockSubscriptionContext> subscriptionContext_ =
|
||||
std::make_shared<StrictMockSubscriptionContext>(tagFactory_);
|
||||
};
|
||||
|
||||
TEST_F(ng_RPCServerHandlerWsTest, HandleRequest_Successful_WsRequest)
|
||||
{
|
||||
backend->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
Request::HttpHeaders headers;
|
||||
auto const request = Request(R"json({"method":"some_method", "id": 1234, "api_version": 1})json", headers);
|
||||
|
||||
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::warnRPC_CLIO);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ng_RPCServerHandlerWsTest, HandleRequest_Successful_WsRequest_HasError)
|
||||
{
|
||||
backend->setRange(0, 1);
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
Request::HttpHeaders headers;
|
||||
auto const request = Request(R"json({"method":"some_method", "id": 1234, "api_version": 1})json", headers);
|
||||
|
||||
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::warnRPC_CLIO);
|
||||
});
|
||||
}
|
||||
@@ -26,8 +26,10 @@
|
||||
#include <boost/beast/http/verb.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
using namespace web::ng;
|
||||
namespace http = boost::beast::http;
|
||||
@@ -176,6 +178,35 @@ INSTANTIATE_TEST_SUITE_P(
|
||||
tests::util::NameGenerator
|
||||
);
|
||||
|
||||
struct RequestHttpHeadersTest : RequestTest {
|
||||
http::field const headerName_ = http::field::user_agent;
|
||||
std::string const headerValue_ = "clio";
|
||||
};
|
||||
|
||||
TEST_F(RequestHttpHeadersTest, httpHeaders_HttpRequest)
|
||||
{
|
||||
auto httpRequest = http::request<http::string_body>{http::verb::get, "/", 11};
|
||||
httpRequest.set(headerName_, headerValue_);
|
||||
Request const request{std::move(httpRequest)};
|
||||
|
||||
auto const& headersFromRequest = request.httpHeaders();
|
||||
ASSERT_EQ(headersFromRequest.count(headerName_), 1);
|
||||
ASSERT_EQ(std::distance(headersFromRequest.cbegin(), headersFromRequest.cend()), 1);
|
||||
EXPECT_EQ(headersFromRequest.at(headerName_), headerValue_);
|
||||
}
|
||||
|
||||
TEST_F(RequestHttpHeadersTest, httpHeaders_WsRequest)
|
||||
{
|
||||
Request::HttpHeaders headers;
|
||||
headers.set(headerName_, headerValue_);
|
||||
Request const request{"websocket message", headers};
|
||||
|
||||
auto const& headersFromRequest = request.httpHeaders();
|
||||
ASSERT_EQ(std::distance(headersFromRequest.cbegin(), headersFromRequest.cend()), 1);
|
||||
ASSERT_EQ(headersFromRequest.count(headerName_), 1);
|
||||
EXPECT_EQ(headersFromRequest.at(headerName_), headerValue_);
|
||||
}
|
||||
|
||||
struct RequestHeaderValueTest : RequestTest {};
|
||||
|
||||
TEST_F(RequestHeaderValueTest, headerValue)
|
||||
|
||||
@@ -50,7 +50,7 @@ TEST_F(ResponseDeathTest, asConstBufferWithHttpData)
|
||||
{
|
||||
Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
|
||||
web::ng::Response const response{boost::beast::http::status::ok, "message", request};
|
||||
EXPECT_DEATH(response.asConstBuffer(), "");
|
||||
EXPECT_DEATH(response.asWsResponse(), "");
|
||||
}
|
||||
|
||||
struct ResponseTest : testing::Test {
|
||||
@@ -104,7 +104,7 @@ TEST_F(ResponseTest, asConstBuffer)
|
||||
std::string const responseMessage = "response message";
|
||||
web::ng::Response const response{responseStatus_, responseMessage, request};
|
||||
|
||||
auto const buffer = response.asConstBuffer();
|
||||
auto const buffer = response.asWsResponse();
|
||||
EXPECT_EQ(buffer.size(), responseMessage.size());
|
||||
|
||||
std::string const messageFromBuffer{static_cast<char const*>(buffer.data()), buffer.size()};
|
||||
@@ -117,7 +117,7 @@ TEST_F(ResponseTest, asConstBufferJson)
|
||||
boost::json::object const responseMessage{{"key", "value"}};
|
||||
web::ng::Response const response{responseStatus_, responseMessage, request};
|
||||
|
||||
auto const buffer = response.asConstBuffer();
|
||||
auto const buffer = response.asWsResponse();
|
||||
EXPECT_EQ(buffer.size(), boost::json::serialize(responseMessage).size());
|
||||
|
||||
std::string const messageFromBuffer{static_cast<char const*>(buffer.data()), buffer.size()};
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
#include "util/TestHttpClient.hpp"
|
||||
#include "util/TestWebSocketClient.hpp"
|
||||
#include "util/config/Config.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"
|
||||
@@ -48,7 +50,6 @@
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
using namespace web::ng;
|
||||
|
||||
@@ -164,20 +165,24 @@ struct ServerTest : SyncAsioContextTest {
|
||||
std::string const headerName_ = "Some-header";
|
||||
std::string const headerValue_ = "some value";
|
||||
|
||||
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
getHandler_;
|
||||
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
postHandler_;
|
||||
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
|
||||
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};
|
||||
impl::ConnectionHandler connectionHandler{impl::ConnectionHandler::ProcessingPolicy::Sequential, std::nullopt};
|
||||
util::TagDecoratorFactory const tagDecoratorFactory{util::Config{boost::json::value{}}};
|
||||
Server server{ctx, endpoint, std::nullopt, std::move(connectionHandler), tagDecoratorFactory};
|
||||
Server server{
|
||||
ctx, endpoint, std::nullopt, ProcessingPolicy::Sequential, std::nullopt, tagDecoratorFactory, std::nullopt
|
||||
};
|
||||
auto maybeError = server.run();
|
||||
ASSERT_TRUE(maybeError.has_value());
|
||||
EXPECT_THAT(*maybeError, testing::HasSubstr("Error creating TCP acceptor"));
|
||||
@@ -251,7 +256,7 @@ TEST_P(ServerHttpTest, RequestResponse)
|
||||
|
||||
EXPECT_CALL(handler, Call)
|
||||
.Times(3)
|
||||
.WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&) {
|
||||
.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());
|
||||
@@ -317,7 +322,7 @@ TEST_F(ServerTest, WsRequestResponse)
|
||||
|
||||
EXPECT_CALL(wsHandler_, Call)
|
||||
.Times(3)
|
||||
.WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&) {
|
||||
.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_);
|
||||
|
||||
166
tests/unit/web/ng/SubscriptionContextTests.cpp
Normal file
166
tests/unit/web/ng/SubscriptionContextTests.cpp
Normal file
@@ -0,0 +1,166 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/Config.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;
|
||||
|
||||
struct ng_SubscriptionContextTests : SyncAsioContextTest {
|
||||
util::TagDecoratorFactory tagFactory_{util::Config{}};
|
||||
MockWsConnectionImpl connection_{"some ip", boost::beast::flat_buffer{}, tagFactory_};
|
||||
testing::StrictMock<testing::MockFunction<bool(Error const&, Connection const&)>> errorHandler_;
|
||||
|
||||
SubscriptionContext
|
||||
makeSubscriptionContext(boost::asio::yield_context yield, std::optional<size_t> maxSendQueueSize = std::nullopt)
|
||||
{
|
||||
return SubscriptionContext{tagFactory_, connection_, maxSendQueueSize, yield, errorHandler_.AsStdFunction()};
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ng_SubscriptionContextTests, 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, auto) {
|
||||
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
|
||||
return std::nullopt;
|
||||
});
|
||||
subscriptionContext.send(message);
|
||||
subscriptionContext.disconnect(yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ng_SubscriptionContextTests, 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 sequence;
|
||||
EXPECT_CALL(connection_, sendBuffer)
|
||||
.InSequence(sequence)
|
||||
.WillOnce([&message1](boost::asio::const_buffer buffer, auto, 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, auto) {
|
||||
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message2);
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
subscriptionContext.send(message1);
|
||||
subscriptionContext.send(message2);
|
||||
subscriptionContext.disconnect(yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ng_SubscriptionContextTests, 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, 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(ng_SubscriptionContextTests, 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, auto) {
|
||||
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(ng_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(ng_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(ng_SubscriptionContextTests, SetApiSubversion)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto subscriptionContext = makeSubscriptionContext(yield);
|
||||
subscriptionContext.setApiSubversion(42);
|
||||
EXPECT_EQ(subscriptionContext.apiSubversion(), 42);
|
||||
});
|
||||
}
|
||||
@@ -21,16 +21,21 @@
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/UnsupportedType.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
#include "web/SubscriptionContextInterface.hpp"
|
||||
#include "web/ng/Connection.hpp"
|
||||
#include "web/ng/Error.hpp"
|
||||
#include "web/ng/MockConnection.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 <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/beast/core/buffers_to_string.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/http/error.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
@@ -58,8 +63,8 @@ namespace http = boost::beast::http;
|
||||
namespace websocket = boost::beast::websocket;
|
||||
|
||||
struct ConnectionHandlerTest : SyncAsioContextTest {
|
||||
ConnectionHandlerTest(ConnectionHandler::ProcessingPolicy policy, std::optional<size_t> maxParallelConnections)
|
||||
: connectionHandler_{policy, maxParallelConnections}
|
||||
ConnectionHandlerTest(ProcessingPolicy policy, std::optional<size_t> maxParallelConnections)
|
||||
: tagFactory_{util::Config{}}, connectionHandler_{policy, maxParallelConnections, tagFactory_, std::nullopt}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -88,65 +93,71 @@ struct ConnectionHandlerTest : SyncAsioContextTest {
|
||||
return Request{std::forward<Args>(args)...};
|
||||
}
|
||||
|
||||
util::TagDecoratorFactory tagFactory_;
|
||||
ConnectionHandler connectionHandler_;
|
||||
|
||||
util::TagDecoratorFactory tagDecoratorFactory_{util::Config(boost::json::object{{"log_tag_style", "uint"}})};
|
||||
StrictMockConnectionPtr mockConnection_ =
|
||||
std::make_unique<StrictMockConnection>("1.2.3.4", beast::flat_buffer{}, tagDecoratorFactory_);
|
||||
StrictMockHttpConnectionPtr mockHttpConnection_ =
|
||||
std::make_unique<StrictMockHttpConnection>("1.2.3.4", beast::flat_buffer{}, tagDecoratorFactory_);
|
||||
StrictMockWsConnectionPtr mockWsConnection_ =
|
||||
std::make_unique<StrictMockWsConnection>("1.2.3.4", beast::flat_buffer{}, tagDecoratorFactory_);
|
||||
};
|
||||
|
||||
struct ConnectionHandlerSequentialProcessingTest : ConnectionHandlerTest {
|
||||
ConnectionHandlerSequentialProcessingTest()
|
||||
: ConnectionHandlerTest(ConnectionHandler::ProcessingPolicy::Sequential, std::nullopt)
|
||||
ConnectionHandlerSequentialProcessingTest() : ConnectionHandlerTest(ProcessingPolicy::Sequential, std::nullopt)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ConnectionHandlerSequentialProcessingTest, ReceiveError)
|
||||
{
|
||||
EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(http::error::end_of_stream)));
|
||||
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
|
||||
EXPECT_CALL(*mockHttpConnection_, receive).WillOnce(Return(makeError(http::error::end_of_stream)));
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockConnection_), yield);
|
||||
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerSequentialProcessingTest, ReceiveError_CloseConnection)
|
||||
{
|
||||
EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(boost::asio::error::timed_out)));
|
||||
EXPECT_CALL(*mockConnection_, close);
|
||||
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
|
||||
EXPECT_CALL(*mockHttpConnection_, receive).WillOnce(Return(makeError(boost::asio::error::timed_out)));
|
||||
EXPECT_CALL(*mockHttpConnection_, close);
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockConnection_), yield);
|
||||
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_NoHandler_Send)
|
||||
{
|
||||
EXPECT_CALL(*mockConnection_, receive)
|
||||
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
|
||||
EXPECT_CALL(*mockHttpConnection_, receive)
|
||||
.WillOnce(Return(makeRequest("some_request", Request::HttpHeaders{})))
|
||||
.WillOnce(Return(makeError(websocket::error::closed)));
|
||||
|
||||
EXPECT_CALL(*mockConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
|
||||
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
|
||||
EXPECT_EQ(response.message(), "WebSocket is not supported by this server");
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockConnection_), yield);
|
||||
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadTarget_Send)
|
||||
{
|
||||
std::string const target = "/some/target";
|
||||
|
||||
std::string const requestMessage = "some message";
|
||||
EXPECT_CALL(*mockConnection_, receive)
|
||||
|
||||
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
|
||||
EXPECT_CALL(*mockHttpConnection_, receive)
|
||||
.WillOnce(Return(makeRequest(http::request<http::string_body>{http::verb::get, target, 11, requestMessage})))
|
||||
.WillOnce(Return(makeError(http::error::end_of_stream)));
|
||||
|
||||
EXPECT_CALL(*mockConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
|
||||
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
|
||||
EXPECT_EQ(response.message(), "Bad target");
|
||||
auto const httpResponse = std::move(response).intoHttpResponse();
|
||||
EXPECT_EQ(httpResponse.result(), http::status::bad_request);
|
||||
@@ -155,57 +166,126 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadTarget_Send)
|
||||
});
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockConnection_), yield);
|
||||
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadMethod_Send)
|
||||
{
|
||||
EXPECT_CALL(*mockConnection_, receive)
|
||||
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
|
||||
EXPECT_CALL(*mockHttpConnection_, receive)
|
||||
.WillOnce(Return(makeRequest(http::request<http::string_body>{http::verb::acl, "/", 11})))
|
||||
.WillOnce(Return(makeError(http::error::end_of_stream)));
|
||||
|
||||
EXPECT_CALL(*mockConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
|
||||
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
|
||||
EXPECT_EQ(response.message(), "Unsupported http method");
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockConnection_), yield);
|
||||
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send)
|
||||
{
|
||||
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
wsHandlerMock;
|
||||
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
|
||||
|
||||
std::string const requestMessage = "some message";
|
||||
std::string const responseMessage = "some response";
|
||||
EXPECT_CALL(*mockConnection_, receive)
|
||||
|
||||
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
|
||||
EXPECT_CALL(*mockWsConnection_, receive)
|
||||
.WillOnce(Return(makeRequest(requestMessage, Request::HttpHeaders{})))
|
||||
.WillOnce(Return(makeError(websocket::error::closed)));
|
||||
|
||||
EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&) {
|
||||
EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&, auto&&) {
|
||||
EXPECT_EQ(request.message(), requestMessage);
|
||||
return Response(http::status::ok, responseMessage, request);
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mockConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
|
||||
EXPECT_CALL(*mockWsConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
|
||||
EXPECT_EQ(response.message(), responseMessage);
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockConnection_), yield);
|
||||
connectionHandler_.processConnection(std::move(mockWsConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send_Loop)
|
||||
TEST_F(ConnectionHandlerSequentialProcessingTest, SendSubscriptionMessage)
|
||||
{
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
wsHandlerMock;
|
||||
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
|
||||
|
||||
std::string const subscriptionMessage = "subscription message";
|
||||
|
||||
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
|
||||
EXPECT_CALL(*mockWsConnection_, receive)
|
||||
.WillOnce(Return(makeRequest("", Request::HttpHeaders{})))
|
||||
.WillOnce(Return(makeError(websocket::error::closed)));
|
||||
|
||||
EXPECT_CALL(wsHandlerMock, Call)
|
||||
.WillOnce([&](Request const& request, auto&&, web::SubscriptionContextPtr subscriptionContext, auto&&) {
|
||||
EXPECT_NE(subscriptionContext, nullptr);
|
||||
subscriptionContext->send(std::make_shared<std::string>(subscriptionMessage));
|
||||
return Response(http::status::ok, "", request);
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mockWsConnection_, send).WillOnce(Return(std::nullopt));
|
||||
|
||||
EXPECT_CALL(*mockWsConnection_, sendBuffer)
|
||||
.WillOnce([&subscriptionMessage](boost::asio::const_buffer buffer, auto&&, auto&&) {
|
||||
EXPECT_EQ(boost::beast::buffers_to_string(buffer), subscriptionMessage);
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockWsConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerSequentialProcessingTest, SubscriptionContextIsDisconnectedAfterProcessingFinished)
|
||||
{
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
wsHandlerMock;
|
||||
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
|
||||
|
||||
testing::StrictMock<testing::MockFunction<void(web::SubscriptionContextInterface*)>> onDisconnectHook;
|
||||
|
||||
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
|
||||
testing::Expectation const expectationReceiveCalled = EXPECT_CALL(*mockWsConnection_, receive)
|
||||
.WillOnce(Return(makeRequest("", Request::HttpHeaders{})))
|
||||
.WillOnce(Return(makeError(websocket::error::closed)));
|
||||
|
||||
EXPECT_CALL(wsHandlerMock, Call)
|
||||
.WillOnce([&](Request const& request, auto&&, web::SubscriptionContextPtr subscriptionContext, auto&&) {
|
||||
EXPECT_NE(subscriptionContext, nullptr);
|
||||
subscriptionContext->onDisconnect(onDisconnectHook.AsStdFunction());
|
||||
return Response(http::status::ok, "", request);
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mockWsConnection_, send).WillOnce(Return(std::nullopt));
|
||||
|
||||
EXPECT_CALL(onDisconnectHook, Call).After(expectationReceiveCalled);
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockWsConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerSequentialProcessingTest, SubscriptionContextIsNullForHttpConnection)
|
||||
{
|
||||
std::string const target = "/some/target";
|
||||
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
postHandlerMock;
|
||||
connectionHandler_.onPost(target, postHandlerMock.AsStdFunction());
|
||||
|
||||
@@ -214,33 +294,76 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send_Loop)
|
||||
|
||||
auto const returnRequest =
|
||||
Return(makeRequest(http::request<http::string_body>{http::verb::post, target, 11, requestMessage}));
|
||||
EXPECT_CALL(*mockConnection_, receive)
|
||||
|
||||
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
|
||||
EXPECT_CALL(*mockHttpConnection_, receive)
|
||||
.WillOnce(returnRequest)
|
||||
.WillOnce(Return(makeError(http::error::partial_message)));
|
||||
|
||||
EXPECT_CALL(postHandlerMock, Call)
|
||||
.WillOnce([&](Request const& request, auto&&, web::SubscriptionContextPtr subscriptionContext, auto&&) {
|
||||
EXPECT_EQ(subscriptionContext, nullptr);
|
||||
|
||||
return Response(http::status::ok, responseMessage, request);
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
|
||||
EXPECT_EQ(response.message(), responseMessage);
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mockHttpConnection_, close);
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send_Loop)
|
||||
{
|
||||
std::string const target = "/some/target";
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
postHandlerMock;
|
||||
connectionHandler_.onPost(target, postHandlerMock.AsStdFunction());
|
||||
|
||||
std::string const requestMessage = "some message";
|
||||
std::string const responseMessage = "some response";
|
||||
|
||||
auto const returnRequest =
|
||||
Return(makeRequest(http::request<http::string_body>{http::verb::post, target, 11, requestMessage}));
|
||||
|
||||
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
|
||||
EXPECT_CALL(*mockHttpConnection_, receive)
|
||||
.WillOnce(returnRequest)
|
||||
.WillOnce(returnRequest)
|
||||
.WillOnce(returnRequest)
|
||||
.WillOnce(Return(makeError(http::error::partial_message)));
|
||||
|
||||
EXPECT_CALL(postHandlerMock, Call).Times(3).WillRepeatedly([&](Request const& request, auto&&, auto&&) {
|
||||
EXPECT_CALL(postHandlerMock, Call).Times(3).WillRepeatedly([&](Request const& request, auto&&, auto&&, auto&&) {
|
||||
EXPECT_EQ(request.message(), requestMessage);
|
||||
return Response(http::status::ok, responseMessage, request);
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mockConnection_, send).Times(3).WillRepeatedly([&responseMessage](Response response, auto&&, auto&&) {
|
||||
EXPECT_EQ(response.message(), responseMessage);
|
||||
return std::nullopt;
|
||||
});
|
||||
EXPECT_CALL(*mockHttpConnection_, send)
|
||||
.Times(3)
|
||||
.WillRepeatedly([&responseMessage](Response response, auto&&, auto&&) {
|
||||
EXPECT_EQ(response.message(), responseMessage);
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mockConnection_, close);
|
||||
EXPECT_CALL(*mockHttpConnection_, close);
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockConnection_), yield);
|
||||
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_SendError)
|
||||
{
|
||||
std::string const target = "/some/target";
|
||||
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
getHandlerMock;
|
||||
|
||||
std::string const requestMessage = "some message";
|
||||
@@ -248,34 +371,38 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_SendError)
|
||||
|
||||
connectionHandler_.onGet(target, getHandlerMock.AsStdFunction());
|
||||
|
||||
EXPECT_CALL(*mockConnection_, receive)
|
||||
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
|
||||
EXPECT_CALL(*mockHttpConnection_, receive)
|
||||
.WillOnce(Return(makeRequest(http::request<http::string_body>{http::verb::get, target, 11, requestMessage})));
|
||||
|
||||
EXPECT_CALL(getHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&) {
|
||||
EXPECT_CALL(getHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&, auto&&) {
|
||||
EXPECT_EQ(request.message(), requestMessage);
|
||||
return Response(http::status::ok, responseMessage, request);
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mockConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
|
||||
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
|
||||
EXPECT_EQ(response.message(), responseMessage);
|
||||
return makeError(http::error::end_of_stream).error();
|
||||
});
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockConnection_), yield);
|
||||
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
|
||||
{
|
||||
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
wsHandlerMock;
|
||||
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
|
||||
|
||||
std::string const requestMessage = "some message";
|
||||
std::string const responseMessage = "some response";
|
||||
bool connectionClosed = false;
|
||||
EXPECT_CALL(*mockConnection_, receive)
|
||||
|
||||
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
|
||||
EXPECT_CALL(*mockWsConnection_, receive)
|
||||
.Times(4)
|
||||
.WillRepeatedly([&](auto&&, auto&&) -> std::expected<Request, Error> {
|
||||
if (connectionClosed) {
|
||||
@@ -284,13 +411,13 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
|
||||
return makeRequest(requestMessage, Request::HttpHeaders{});
|
||||
});
|
||||
|
||||
EXPECT_CALL(wsHandlerMock, Call).Times(3).WillRepeatedly([&](Request const& request, auto&&, auto&&) {
|
||||
EXPECT_CALL(wsHandlerMock, Call).Times(3).WillRepeatedly([&](Request const& request, auto&&, auto&&, auto&&) {
|
||||
EXPECT_EQ(request.message(), requestMessage);
|
||||
return Response(http::status::ok, responseMessage, request);
|
||||
});
|
||||
|
||||
size_t numCalls = 0;
|
||||
EXPECT_CALL(*mockConnection_, send).Times(3).WillRepeatedly([&](Response response, auto&&, auto&&) {
|
||||
EXPECT_CALL(*mockWsConnection_, send).Times(3).WillRepeatedly([&](Response response, auto&&, auto&&) {
|
||||
EXPECT_EQ(response.message(), responseMessage);
|
||||
|
||||
++numCalls;
|
||||
@@ -300,10 +427,10 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mockConnection_, close).WillOnce([&connectionClosed]() { connectionClosed = true; });
|
||||
EXPECT_CALL(*mockWsConnection_, close).WillOnce([&connectionClosed]() { connectionClosed = true; });
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockConnection_), yield);
|
||||
connectionHandler_.processConnection(std::move(mockWsConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -312,7 +439,7 @@ struct ConnectionHandlerParallelProcessingTest : ConnectionHandlerTest {
|
||||
|
||||
ConnectionHandlerParallelProcessingTest()
|
||||
: ConnectionHandlerTest(
|
||||
ConnectionHandler::ProcessingPolicy::Parallel,
|
||||
ProcessingPolicy::Parallel,
|
||||
ConnectionHandlerParallelProcessingTest::maxParallelRequests
|
||||
)
|
||||
{
|
||||
@@ -329,43 +456,48 @@ struct ConnectionHandlerParallelProcessingTest : ConnectionHandlerTest {
|
||||
|
||||
TEST_F(ConnectionHandlerParallelProcessingTest, ReceiveError)
|
||||
{
|
||||
EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(http::error::end_of_stream)));
|
||||
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
|
||||
EXPECT_CALL(*mockHttpConnection_, receive).WillOnce(Return(makeError(http::error::end_of_stream)));
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockConnection_), yield);
|
||||
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send)
|
||||
{
|
||||
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
wsHandlerMock;
|
||||
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
|
||||
|
||||
std::string const requestMessage = "some message";
|
||||
std::string const responseMessage = "some response";
|
||||
EXPECT_CALL(*mockConnection_, receive)
|
||||
|
||||
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
|
||||
EXPECT_CALL(*mockWsConnection_, receive)
|
||||
.WillOnce(Return(makeRequest(requestMessage, Request::HttpHeaders{})))
|
||||
.WillOnce(Return(makeError(websocket::error::closed)));
|
||||
|
||||
EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&) {
|
||||
EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&, auto&&) {
|
||||
EXPECT_EQ(request.message(), requestMessage);
|
||||
return Response(http::status::ok, responseMessage, request);
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mockConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
|
||||
EXPECT_CALL(*mockWsConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
|
||||
EXPECT_EQ(response.message(), responseMessage);
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockConnection_), yield);
|
||||
connectionHandler_.processConnection(std::move(mockWsConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop)
|
||||
{
|
||||
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
wsHandlerMock;
|
||||
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
|
||||
|
||||
@@ -373,29 +505,34 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop)
|
||||
std::string const responseMessage = "some response";
|
||||
|
||||
auto const returnRequest = [&](auto&&, auto&&) { return makeRequest(requestMessage, Request::HttpHeaders{}); };
|
||||
EXPECT_CALL(*mockConnection_, receive)
|
||||
|
||||
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
|
||||
EXPECT_CALL(*mockWsConnection_, receive)
|
||||
.WillOnce(returnRequest)
|
||||
.WillOnce(returnRequest)
|
||||
.WillOnce(Return(makeError(websocket::error::closed)));
|
||||
|
||||
EXPECT_CALL(wsHandlerMock, Call).Times(2).WillRepeatedly([&](Request const& request, auto&&, auto&&) {
|
||||
EXPECT_CALL(wsHandlerMock, Call).Times(2).WillRepeatedly([&](Request const& request, auto&&, auto&&, auto&&) {
|
||||
EXPECT_EQ(request.message(), requestMessage);
|
||||
return Response(http::status::ok, responseMessage, request);
|
||||
});
|
||||
|
||||
EXPECT_CALL(*mockConnection_, send).Times(2).WillRepeatedly([&responseMessage](Response response, auto&&, auto&&) {
|
||||
EXPECT_EQ(response.message(), responseMessage);
|
||||
return std::nullopt;
|
||||
});
|
||||
EXPECT_CALL(*mockWsConnection_, send)
|
||||
.Times(2)
|
||||
.WillRepeatedly([&responseMessage](Response response, auto&&, auto&&) {
|
||||
EXPECT_EQ(response.message(), responseMessage);
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockConnection_), yield);
|
||||
connectionHandler_.processConnection(std::move(mockWsConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooManyRequest)
|
||||
{
|
||||
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
|
||||
testing::StrictMock<testing::MockFunction<
|
||||
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
|
||||
wsHandlerMock;
|
||||
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
|
||||
|
||||
@@ -404,7 +541,9 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooMany
|
||||
|
||||
auto const returnRequest = [&](auto&&, auto&&) { return makeRequest(requestMessage, Request::HttpHeaders{}); };
|
||||
testing::Sequence const sequence;
|
||||
EXPECT_CALL(*mockConnection_, receive)
|
||||
|
||||
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
|
||||
EXPECT_CALL(*mockWsConnection_, receive)
|
||||
.WillOnce(returnRequest)
|
||||
.WillOnce(returnRequest)
|
||||
.WillOnce(returnRequest)
|
||||
@@ -414,14 +553,14 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooMany
|
||||
|
||||
EXPECT_CALL(wsHandlerMock, Call)
|
||||
.Times(3)
|
||||
.WillRepeatedly([&](Request const& request, auto&&, boost::asio::yield_context yield) {
|
||||
.WillRepeatedly([&](Request const& request, auto&&, auto&&, boost::asio::yield_context yield) {
|
||||
EXPECT_EQ(request.message(), requestMessage);
|
||||
asyncSleep(yield, std::chrono::milliseconds{3});
|
||||
return Response(http::status::ok, responseMessage, request);
|
||||
});
|
||||
|
||||
EXPECT_CALL(
|
||||
*mockConnection_,
|
||||
*mockWsConnection_,
|
||||
send(
|
||||
testing::ResultOf([](Response response) { return response.message(); }, responseMessage),
|
||||
testing::_,
|
||||
@@ -432,7 +571,7 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooMany
|
||||
.WillRepeatedly(Return(std::nullopt));
|
||||
|
||||
EXPECT_CALL(
|
||||
*mockConnection_,
|
||||
*mockWsConnection_,
|
||||
send(
|
||||
testing::ResultOf(
|
||||
[](Response response) { return response.message(); }, "Too many requests for one connection"
|
||||
@@ -445,6 +584,6 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooMany
|
||||
.WillRepeatedly(Return(std::nullopt));
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
connectionHandler_.processConnection(std::move(mockConnection_), yield);
|
||||
connectionHandler_.processConnection(std::move(mockWsConnection_), yield);
|
||||
});
|
||||
}
|
||||
|
||||
358
tests/unit/web/ng/impl/ErrorHandlingTests.cpp
Normal file
358
tests/unit/web/ng/impl/ErrorHandlingTests.cpp
Normal file
@@ -0,0 +1,358 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 <gmock/gmock.h>
|
||||
#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 ng_ErrorHandlingTests : 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("")}};
|
||||
return Request{body.value_or(""), Request::HttpHeaders{}};
|
||||
}
|
||||
};
|
||||
|
||||
struct ng_ErrorHandlingMakeErrorTestBundle {
|
||||
std::string testName;
|
||||
bool isHttp;
|
||||
rpc::Status status;
|
||||
std::string expectedMessage;
|
||||
boost::beast::http::status expectedStatus;
|
||||
};
|
||||
|
||||
struct ng_ErrorHandlingMakeErrorTest : ng_ErrorHandlingTests,
|
||||
testing::WithParamInterface<ng_ErrorHandlingMakeErrorTestBundle> {};
|
||||
|
||||
TEST_P(ng_ErrorHandlingMakeErrorTest, MakeError)
|
||||
{
|
||||
auto const request = makeRequest(GetParam().isHttp);
|
||||
ErrorHelper 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,
|
||||
ng_ErrorHandlingMakeErrorTest,
|
||||
testing::ValuesIn({
|
||||
ng_ErrorHandlingMakeErrorTestBundle{
|
||||
"WsRequest",
|
||||
false,
|
||||
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
|
||||
R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})",
|
||||
boost::beast::http::status::ok
|
||||
},
|
||||
ng_ErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_InvalidApiVersion",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::rpcINVALID_API_VERSION},
|
||||
"invalid_API_version",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ng_ErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_CommandIsMissing",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::rpcCOMMAND_IS_MISSING},
|
||||
"Null method",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ng_ErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_CommandIsEmpty",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::rpcCOMMAND_IS_EMPTY},
|
||||
"method is empty",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ng_ErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_CommandNotString",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::rpcCOMMAND_NOT_STRING},
|
||||
"method is not string",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ng_ErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_ParamsUnparseable",
|
||||
true,
|
||||
rpc::Status{rpc::ClioError::rpcPARAMS_UNPARSEABLE},
|
||||
"params unparseable",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
ng_ErrorHandlingMakeErrorTestBundle{
|
||||
"HttpRequest_RippledError",
|
||||
true,
|
||||
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
|
||||
R"({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})",
|
||||
boost::beast::http::status::bad_request
|
||||
},
|
||||
}),
|
||||
tests::util::NameGenerator
|
||||
);
|
||||
|
||||
struct ng_ErrorHandlingMakeInternalErrorTestBundle {
|
||||
std::string testName;
|
||||
bool isHttp;
|
||||
std::optional<std::string> request;
|
||||
boost::json::object expectedResult;
|
||||
};
|
||||
|
||||
struct ng_ErrorHandlingMakeInternalErrorTest
|
||||
: ng_ErrorHandlingTests,
|
||||
testing::WithParamInterface<ng_ErrorHandlingMakeInternalErrorTestBundle> {};
|
||||
|
||||
TEST_P(ng_ErrorHandlingMakeInternalErrorTest, 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 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,
|
||||
ng_ErrorHandlingMakeInternalErrorTest,
|
||||
testing::ValuesIn(
|
||||
{ng_ErrorHandlingMakeInternalErrorTestBundle{
|
||||
"NoRequest_WebsocketConnection",
|
||||
false,
|
||||
std::nullopt,
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"}}
|
||||
},
|
||||
ng_ErrorHandlingMakeInternalErrorTestBundle{
|
||||
"NoRequest_HttpConnection",
|
||||
true,
|
||||
std::nullopt,
|
||||
{{"result",
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"}}}}
|
||||
},
|
||||
ng_ErrorHandlingMakeInternalErrorTestBundle{
|
||||
"Request_WebsocketConnection",
|
||||
false,
|
||||
std::string{R"({"id": 1, "api_version": 2})"},
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"},
|
||||
{"id", 1},
|
||||
{"api_version", 2},
|
||||
{"request", {{"id", 1}, {"api_version", 2}}}}
|
||||
},
|
||||
ng_ErrorHandlingMakeInternalErrorTestBundle{
|
||||
"Request_WebsocketConnection_NoId",
|
||||
false,
|
||||
std::string{R"({"api_version": 2})"},
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"},
|
||||
{"api_version", 2},
|
||||
{"request", {{"api_version", 2}}}}
|
||||
},
|
||||
ng_ErrorHandlingMakeInternalErrorTestBundle{
|
||||
"Request_HttpConnection",
|
||||
true,
|
||||
std::string{R"({"id": 1, "api_version": 2})"},
|
||||
{{"result",
|
||||
{{"error", "internal"},
|
||||
{"error_code", 73},
|
||||
{"error_message", "Internal error."},
|
||||
{"status", "error"},
|
||||
{"type", "response"},
|
||||
{"id", 1},
|
||||
{"request", {{"id", 1}, {"api_version", 2}}}}}}
|
||||
}}
|
||||
),
|
||||
tests::util::NameGenerator
|
||||
);
|
||||
|
||||
TEST_F(ng_ErrorHandlingTests, MakeNotReadyError)
|
||||
{
|
||||
auto const request = makeRequest(true);
|
||||
auto response = ErrorHelper{request}.makeNotReadyError();
|
||||
EXPECT_EQ(
|
||||
response.message(),
|
||||
std::string{
|
||||
R"({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})"
|
||||
}
|
||||
);
|
||||
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(ng_ErrorHandlingTests, MakeTooBusyError_WebsocketRequest)
|
||||
{
|
||||
auto const request = makeRequest(false);
|
||||
auto response = ErrorHelper{request}.makeTooBusyError();
|
||||
EXPECT_EQ(
|
||||
response.message(),
|
||||
std::string{
|
||||
R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(ng_ErrorHandlingTests, sendTooBusyError_HttpConnection)
|
||||
{
|
||||
auto const request = makeRequest(true);
|
||||
auto response = ErrorHelper{request}.makeTooBusyError();
|
||||
EXPECT_EQ(
|
||||
response.message(),
|
||||
std::string{
|
||||
R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})"
|
||||
}
|
||||
);
|
||||
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(ng_ErrorHandlingTests, makeJsonParsingError_WebsocketConnection)
|
||||
{
|
||||
auto const request = makeRequest(false);
|
||||
auto response = ErrorHelper{request}.makeJsonParsingError();
|
||||
EXPECT_EQ(
|
||||
response.message(),
|
||||
std::string{
|
||||
R"({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(ng_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 ng_ErrorHandlingComposeErrorTestBundle {
|
||||
std::string testName;
|
||||
bool isHttp;
|
||||
std::optional<boost::json::object> request;
|
||||
std::string expectedMessage;
|
||||
};
|
||||
|
||||
struct ng_ErrorHandlingComposeErrorTest : ng_ErrorHandlingTests,
|
||||
testing::WithParamInterface<ng_ErrorHandlingComposeErrorTestBundle> {};
|
||||
|
||||
TEST_P(ng_ErrorHandlingComposeErrorTest, ComposeError)
|
||||
{
|
||||
auto const request = makeRequest(GetParam().isHttp);
|
||||
ErrorHelper 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,
|
||||
ng_ErrorHandlingComposeErrorTest,
|
||||
testing::ValuesIn(
|
||||
{ng_ErrorHandlingComposeErrorTestBundle{
|
||||
"NoRequest_WebsocketConnection",
|
||||
false,
|
||||
std::nullopt,
|
||||
R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})"
|
||||
},
|
||||
ng_ErrorHandlingComposeErrorTestBundle{
|
||||
"NoRequest_HttpConnection",
|
||||
true,
|
||||
std::nullopt,
|
||||
R"({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"}})"
|
||||
},
|
||||
ng_ErrorHandlingComposeErrorTestBundle{
|
||||
"Request_WebsocketConnection",
|
||||
false,
|
||||
boost::json::object{{"id", 1}, {"api_version", 2}},
|
||||
R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"api_version":2,"request":{"id":1,"api_version":2}})",
|
||||
},
|
||||
ng_ErrorHandlingComposeErrorTestBundle{
|
||||
"Request_WebsocketConnection_NoId",
|
||||
false,
|
||||
boost::json::object{{"api_version", 2}},
|
||||
R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","api_version":2,"request":{"api_version":2}})",
|
||||
},
|
||||
ng_ErrorHandlingComposeErrorTestBundle{
|
||||
"Request_HttpConnection",
|
||||
true,
|
||||
boost::json::object{{"id", 1}, {"api_version", 2}},
|
||||
R"({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"request":{"id":1,"api_version":2}}})"
|
||||
}}
|
||||
),
|
||||
tests::util::NameGenerator
|
||||
);
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "web/ng/Response.hpp"
|
||||
#include "web/ng/impl/HttpConnection.hpp"
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
@@ -37,6 +38,7 @@
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/http/verb.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
@@ -94,7 +96,6 @@ TEST_F(HttpConnectionTests, Receive)
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto connection = acceptConnection(yield);
|
||||
EXPECT_TRUE(connection.ip() == "127.0.0.1" or connection.ip() == "::1") << connection.ip();
|
||||
|
||||
auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{100});
|
||||
ASSERT_TRUE(expectedRequest.has_value()) << expectedRequest.error().message();
|
||||
@@ -293,3 +294,39 @@ TEST_F(HttpConnectionTests, Upgrade)
|
||||
[&]() { ASSERT_TRUE(expectedWsConnection.has_value()) << expectedWsConnection.error().message(); }();
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(HttpConnectionTests, Ip)
|
||||
{
|
||||
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) mutable {
|
||||
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
|
||||
});
|
||||
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto connection = acceptConnection(yield);
|
||||
EXPECT_TRUE(connection.ip() == "127.0.0.1" or connection.ip() == "::1") << connection.ip();
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(HttpConnectionTests, isAdminSetAdmin)
|
||||
{
|
||||
testing::StrictMock<testing::MockFunction<bool()>> adminSetter;
|
||||
EXPECT_CALL(adminSetter, Call).WillOnce(testing::Return(true));
|
||||
|
||||
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) mutable {
|
||||
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
|
||||
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
|
||||
});
|
||||
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
auto connection = acceptConnection(yield);
|
||||
EXPECT_FALSE(connection.isAdmin());
|
||||
|
||||
connection.setIsAdmin(adminSetter.AsStdFunction());
|
||||
EXPECT_TRUE(connection.isAdmin());
|
||||
|
||||
// Setter shouldn't not be called here because isAdmin is already set
|
||||
connection.setIsAdmin(adminSetter.AsStdFunction());
|
||||
EXPECT_TRUE(connection.isAdmin());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@
|
||||
#include "web/ng/impl/ServerSslContext.hpp"
|
||||
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <fmt/compile.h>
|
||||
#include <fmt/core.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <test_data/SslCert.hpp>
|
||||
|
||||
Reference in New Issue
Block a user