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:
Sergey Kuznetsov
2024-11-21 14:48:32 +00:00
committed by GitHub
parent fc3ba07f2e
commit c77154a5e6
90 changed files with 4029 additions and 683 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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.
};
/**

View File

@@ -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.

View File

@@ -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

View File

@@ -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>;

View File

@@ -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); });
}
}

View File

@@ -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);
});
};

View File

@@ -23,6 +23,7 @@
#include <boost/signals2.hpp>
#include <concepts>
#include <cstddef>
#include <functional>
#include <memory>

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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."}};

View File

@@ -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
);
/**

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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(

View File

@@ -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
);

View File

@@ -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,

View 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

View 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

View 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

View File

@@ -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>

View File

@@ -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)
{

View File

@@ -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

View File

@@ -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.
*/

View File

@@ -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.
*

View File

@@ -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)}
{
}

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -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
{

View File

@@ -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.
*

View File

@@ -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

View File

@@ -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

View File

@@ -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
};
}

View File

@@ -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
);
/**

View 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

View 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

View File

@@ -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};
}

View File

@@ -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

View 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

View 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

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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)
{
}
};

View File

@@ -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>

View 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>>;

View File

@@ -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;

View 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>>;

View 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>>;

View File

@@ -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

View File

@@ -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 :

View File

@@ -41,6 +41,7 @@ using FeedBookChangeTest = FeedBaseTest<BookChangesFeed>;
TEST_F(FeedBookChangeTest, Pub)
{
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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));

View File

@@ -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();
}

View File

@@ -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);
});
}

View File

@@ -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;
};

View File

@@ -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();
});
}

View File

@@ -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
);

View File

@@ -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": {

View File

@@ -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>

View 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);
}

View 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();
}

View 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);
});
}

View File

@@ -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)

View File

@@ -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()};

View File

@@ -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_);

View 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);
});
}

View File

@@ -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);
});
}

View 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
);

View File

@@ -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());
});
}

View File

@@ -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>