#include "rpc/Errors.hpp" #include "rpc/common/APIVersion.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/NameGenerator.hpp" #include "util/Taggable.hpp" #include "util/config/ConfigDefinition.hpp" #include "util/config/ConfigValue.hpp" #include "util/config/Types.hpp" #include "web/RPCServerHandler.hpp" #include "web/SubscriptionContextInterface.hpp" #include "web/dosguard/DOSGuardMock.hpp" #include "web/interface/ConnectionBase.hpp" #include #include #include #include #include #include #include #include #include #include #include using namespace web; using namespace util::config; namespace { constexpr auto kMIN_SEQ = 10; constexpr auto kMAX_SEQ = 30; } // namespace struct MockWsBase : public web::ConnectionBase { std::string message; boost::beast::http::status lastStatus = boost::beast::http::status::unknown; size_t slowDownCallsCounter{0}; void send(std::shared_ptr msgType) override { message += std::string(*msgType); lastStatus = boost::beast::http::status::ok; } void send( std::string&& msg, // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved) boost::beast::http::status status = boost::beast::http::status::ok ) override { message += msg; lastStatus = status; } void sendSlowDown(std::string const&) override { ++slowDownCallsCounter; } 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 { util::config::ClioConfigDefinition cfg{ {"log.tag_style", ConfigValue{ConfigType::String}.defaultValue("none")}, {"api_version.default", ConfigValue{ConfigType::Integer}.defaultValue(rpc::kAPI_VERSION_DEFAULT)}, {"api_version.min", ConfigValue{ConfigType::Integer}.defaultValue(rpc::kAPI_VERSION_MIN)}, {"api_version.max", ConfigValue{ConfigType::Integer}.defaultValue(rpc::kAPI_VERSION_MAX)} }; std::shared_ptr rpcEngine = std::make_shared(); std::shared_ptr etl = std::make_shared(); DOSGuardStrictMock dosguard; std::shared_ptr tagFactory = std::make_shared(cfg); std::shared_ptr> handler = std::make_shared>( cfg, backend_, rpcEngine, etl, dosguard ); std::shared_ptr session = std::make_shared(*tagFactory); }; TEST_F(WebRPCServerHandlerTest, HTTPDefaultPath) { static constexpr auto kREQUEST = R"JSON({ "method": "server_info", "params": [{}] })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kRESULT = "{}"; static constexpr auto kRESPONSE = R"JSON({ "result": { "status": "success" }, "warnings": [ { "id": 2001, "message": "This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request" } ] })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(kREQUEST).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, buildResponse(testing::_)) .WillOnce(testing::Return(rpc::Result{boost::json::parse(kRESULT).as_object()})); EXPECT_CALL(*rpcEngine, notifyComplete("server_info", testing::_)).Times(1); EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45)); (*handler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, HTTPRejectedByDosguard) { static constexpr auto kREQUEST = R"JSON({ "method": "server_info", "params": [{}] })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(false)); (*handler)(kREQUEST, session); EXPECT_EQ(session->slowDownCallsCounter, 1); } TEST_F(WebRPCServerHandlerTest, HTTPRejectedByDosguardAfterParsing) { static constexpr auto kREQUEST = R"JSON({ "method": "server_info", "params": [{}] })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), testing::_)) .WillOnce(testing::Return(false)); (*handler)(kREQUEST, session); EXPECT_EQ(session->slowDownCallsCounter, 1); } TEST_F(WebRPCServerHandlerTest, WsNormalPath) { session->upgraded = true; static constexpr auto kREQUEST = R"JSON({ "command": "server_info", "id": 99, "api_version": 2 })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kRESULT = "{}"; static constexpr auto kRESPONSE = R"JSON({ "result": {}, "id": 99, "status": "success", "type": "response", "api_version": 2, "warnings": [ { "id": 2001, "message": "This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request" } ] })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(kREQUEST).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, buildResponse(testing::_)) .WillOnce(testing::Return(rpc::Result{boost::json::parse(kRESULT).as_object()})); EXPECT_CALL(*rpcEngine, notifyComplete("server_info", testing::_)).Times(1); EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45)); (*handler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, WsRejectedByDosguard) { session->upgraded = true; static constexpr auto kREQUEST = R"JSON({ "command": "server_info", "id": 99, "api_version": 2 })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(false)); (*handler)(kREQUEST, session); EXPECT_EQ(session->slowDownCallsCounter, 1); } TEST_F(WebRPCServerHandlerTest, WsRejectedByDosguardAfterParsing) { session->upgraded = true; static constexpr auto kREQUEST = R"JSON({ "command": "server_info", "id": 99, "api_version": 2 })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(kREQUEST).as_object())) .WillOnce(testing::Return(false)); (*handler)(kREQUEST, session); EXPECT_EQ(session->slowDownCallsCounter, 1); } TEST_F(WebRPCServerHandlerTest, HTTPForwardedPath) { static constexpr auto kREQUEST = R"JSON({ "method": "server_info", "params": [{}] })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); // Note: forwarding always goes thru WS API static constexpr auto kRESULT = R"JSON({ "result": { "index": 1 }, "forwarded": true })JSON"; static constexpr auto kRESPONSE = R"JSON({ "result": { "index": 1, "status": "success" }, "forwarded": true, "warnings": [ { "id": 2001, "message": "This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request" } ] })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(kREQUEST).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, buildResponse(testing::_)) .WillOnce(testing::Return(rpc::Result{boost::json::parse(kRESULT).as_object()})); EXPECT_CALL(*rpcEngine, notifyComplete("server_info", testing::_)).Times(1); EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45)); (*handler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, HTTPForwardedErrorPath) { static constexpr auto kREQUEST = R"JSON({ "method": "server_info", "params": [{}] })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); // Note: forwarding always goes thru WS API static constexpr auto kRESULT = R"JSON({ "error": "error", "error_code": 123, "error_message": "error message", "status": "error", "type": "response", "forwarded": true })JSON"; static constexpr auto kRESPONSE = R"JSON({ "result": { "error": "error", "error_code": 123, "error_message": "error message", "status": "error", "type": "response" }, "forwarded": true, "warnings": [ { "id": 2001, "message": "This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request" } ] })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(kREQUEST).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, buildResponse(testing::_)) .WillOnce(testing::Return(rpc::Result{boost::json::parse(kRESULT).as_object()})); EXPECT_CALL(*rpcEngine, notifyComplete("server_info", testing::_)).Times(1); EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45)); (*handler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, WsForwardedPath) { session->upgraded = true; static constexpr auto kREQUEST = R"JSON({ "command": "server_info", "id": 99 })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); // Note: forwarding always goes thru WS API static constexpr auto kRESULT = R"JSON({ "result": { "index": 1 }, "forwarded": true })JSON"; static constexpr auto kRESPONSE = R"JSON({ "result": { "index": 1 }, "forwarded": true, "id": 99, "status": "success", "type": "response", "warnings": [ { "id": 2001, "message": "This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request" } ] })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(kREQUEST).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, buildResponse(testing::_)) .WillOnce(testing::Return(rpc::Result{boost::json::parse(kRESULT).as_object()})); EXPECT_CALL(*rpcEngine, notifyComplete("server_info", testing::_)).Times(1); EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45)); (*handler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, WsForwardedErrorPath) { session->upgraded = true; static constexpr auto kREQUEST = R"JSON({ "command": "server_info", "id": 99 })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); // Note: forwarding always goes thru WS API static constexpr auto kRESULT = R"JSON({ "error": "error", "error_code": 123, "error_message": "error message", "status": "error", "type": "response", "forwarded": true })JSON"; // WS error responses, unlike their successful counterpart, contain everything on top level // without "result" static constexpr auto kRESPONSE = R"JSON({ "error": "error", "error_code": 123, "error_message": "error message", "status": "error", "type": "response", "forwarded": true, "id": 99, "warnings": [ { "id": 2001, "message": "This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request" } ] })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(kREQUEST).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, buildResponse(testing::_)) .WillOnce(testing::Return(rpc::Result{boost::json::parse(kRESULT).as_object()})); // Forwarded errors counted as successful: EXPECT_CALL(*rpcEngine, notifyComplete("server_info", testing::_)).Times(1); EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45)); (*handler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, HTTPErrorPath) { static constexpr auto kRESPONSE = R"JSON({ "result": { "error": "invalidParams", "error_code": 31, "error_message": "ledgerIndexMalformed", "status": "error", "type": "response", "request": { "method": "ledger", "params": [ { "ledger_index": "xx" } ] } }, "warnings": [ { "id": 2001, "message": "This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request" } ] })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kREQUEST_JSON = R"JSON({ "method": "ledger", "params": [ { "ledger_index": "xx" } ] })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL( dosguard, request(session->clientIp(), boost::json::parse(kREQUEST_JSON).as_object()) ) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, buildResponse(testing::_)) .WillOnce( testing::Return( rpc::Result{ rpc::Status{rpc::RippledError::rpcINVALID_PARAMS, "ledgerIndexMalformed"} } ) ); EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45)); (*handler)(kREQUEST_JSON, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, WsErrorPath) { session->upgraded = true; static constexpr auto kRESPONSE = R"JSON({ "id": "123", "error": "invalidParams", "error_code": 31, "error_message": "ledgerIndexMalformed", "status": "error", "type": "response", "api_version": 2, "request": { "command": "ledger", "ledger_index": "xx", "id": "123", "api_version": 2 }, "warnings": [ { "id": 2001, "message": "This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request" } ] })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kREQUEST_JSON = R"JSON({ "command": "ledger", "ledger_index": "xx", "id": "123", "api_version": 2 })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL( dosguard, request(session->clientIp(), boost::json::parse(kREQUEST_JSON).as_object()) ) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, buildResponse(testing::_)) .WillOnce( testing::Return( rpc::Result{ rpc::Status{rpc::RippledError::rpcINVALID_PARAMS, "ledgerIndexMalformed"} } ) ); EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(45)); (*handler)(kREQUEST_JSON, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, HTTPNotReady) { static constexpr auto kREQUEST = R"JSON({ "method": "server_info", "params": [{}] })JSON"; static constexpr auto kRESPONSE = R"JSON({ "result": { "error": "notReady", "error_code": 13, "error_message": "Not ready to handle this request.", "status": "error", "type": "response", "request": { "method": "server_info", "params": [{}] } } })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(kREQUEST).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyNotReady).Times(1); (*handler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, WsNotReady) { session->upgraded = true; static constexpr auto kREQUEST = R"JSON({ "command": "server_info", "id": 99 })JSON"; static constexpr auto kRESPONSE = R"JSON({ "error": "notReady", "error_code": 13, "error_message": "Not ready to handle this request.", "status": "error", "type": "response", "id": 99, "request": { "command": "server_info", "id": 99 } })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(kREQUEST).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyNotReady).Times(1); (*handler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, HTTPBadSyntaxWhenRequestSubscribe) { static constexpr auto kREQUEST = R"JSON({"method": "subscribe"})JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kRESPONSE = R"JSON({ "result": { "error": "badSyntax", "error_code": 1, "error_message": "Subscribe and unsubscribe are only allowed for websocket.", "status": "error", "type": "response", "request": { "method": "subscribe", "params": [{}] } } })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), testing::_)).WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); (*handler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, HTTPMissingCommand) { static constexpr auto kREQUEST = R"JSON({"method2": "server_info"})JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kRESPONSE = "Null method"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), testing::_)).WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); (*handler)(kREQUEST, session); EXPECT_EQ(session->message, kRESPONSE); EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); } TEST_F(WebRPCServerHandlerTest, HTTPCommandNotString) { static constexpr auto kREQUEST = R"JSON({"method": 1})JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kRESPONSE = "method is not string"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), testing::_)).WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); (*handler)(kREQUEST, session); EXPECT_EQ(session->message, kRESPONSE); EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); } TEST_F(WebRPCServerHandlerTest, HTTPCommandIsEmpty) { static constexpr auto kREQUEST = R"JSON({"method": ""})JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kRESPONSE = "method is empty"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), testing::_)).WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); (*handler)(kREQUEST, session); EXPECT_EQ(session->message, kRESPONSE); EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); } TEST_F(WebRPCServerHandlerTest, WsMissingCommand) { session->upgraded = true; static constexpr auto kREQUEST = R"JSON({ "command2": "server_info", "id": 99 })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kRESPONSE = R"JSON({ "error": "missingCommand", "error_code": 6001, "error_message": "Method/Command is not specified or is not a string.", "status": "error", "type": "response", "id": 99, "request": { "command2": "server_info", "id": 99 } })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(kREQUEST).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); (*handler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, HTTPParamsUnparsableNotArray) { static constexpr auto kRESPONSE = "params unparsable"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kREQUEST_JSON = R"JSON({ "method": "ledger", "params": "wrong" })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), testing::_)).WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); (*handler)(kREQUEST_JSON, session); EXPECT_EQ(session->message, kRESPONSE); EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); } TEST_F(WebRPCServerHandlerTest, HTTPParamsUnparsableArrayWithDigit) { static constexpr auto kRESPONSE = "params unparsable"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kREQUEST_JSON = R"JSON({ "method": "ledger", "params": [1] })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), testing::_)).WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); (*handler)(kREQUEST_JSON, session); EXPECT_EQ(session->message, kRESPONSE); EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); } TEST_F(WebRPCServerHandlerTest, HTTPInternalError) { static constexpr auto kRESPONSE = R"JSON({ "result": { "error": "internal", "error_code": 73, "error_message": "Internal error.", "status": "error", "type": "response", "request": { "method": "ledger", "params": [{}] } } })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kREQUEST_JSON = R"JSON({ "method": "ledger", "params": [{}] })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL( dosguard, request(session->clientIp(), boost::json::parse(kREQUEST_JSON).as_object()) ) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyInternalError).Times(1); EXPECT_CALL(*rpcEngine, buildResponse(testing::_)) .Times(1) .WillOnce(testing::Throw(std::runtime_error("MyError"))); (*handler)(kREQUEST_JSON, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, WsInternalError) { session->upgraded = true; static constexpr auto kRESPONSE = R"JSON({ "error": "internal", "error_code": 73, "error_message": "Internal error.", "status": "error", "type": "response", "id": "123", "request": { "command": "ledger", "id": "123" } })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kREQUEST_JSON = R"JSON({ "command": "ledger", "id": "123" })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL( dosguard, request(session->clientIp(), boost::json::parse(kREQUEST_JSON).as_object()) ) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyInternalError).Times(1); EXPECT_CALL(*rpcEngine, buildResponse(testing::_)) .Times(1) .WillOnce(testing::Throw(std::runtime_error("MyError"))); (*handler)(kREQUEST_JSON, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, HTTPOutDated) { static constexpr auto kREQUEST = R"JSON({ "method": "server_info", "params": [{}] })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kRESULT = "{}"; static constexpr auto kRESPONSE = R"JSON({ "result": { "status": "success" }, "warnings": [ { "id": 2001, "message": "This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request" }, { "id": 2002, "message": "This server may be out of date" } ] })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(kREQUEST).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, buildResponse(testing::_)) .WillOnce(testing::Return(rpc::Result{boost::json::parse(kRESULT).as_object()})); EXPECT_CALL(*rpcEngine, notifyComplete("server_info", testing::_)).Times(1); EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(61)); (*handler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, WsOutdated) { session->upgraded = true; static constexpr auto kREQUEST = R"JSON({ "command": "server_info", "id": 99 })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kRESULT = "{}"; static constexpr auto kRESPONSE = R"JSON({ "result": {}, "id": 99, "status": "success", "type": "response", "warnings": [ { "id": 2001, "message": "This is a clio server. clio only serves validated data. If you want to talk to rippled, include 'ledger_index':'current' in your request" }, { "id": 2002, "message": "This server may be out of date" } ] })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(kREQUEST).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, buildResponse(testing::_)) .WillOnce(testing::Return(rpc::Result{boost::json::parse(kRESULT).as_object()})); EXPECT_CALL(*rpcEngine, notifyComplete("server_info", testing::_)).Times(1); EXPECT_CALL(*etl, lastCloseAgeSeconds()).WillOnce(testing::Return(61)); (*handler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, WsTooBusy) { session->upgraded = true; auto localRpcEngine = std::make_shared(); auto localHandler = std::make_shared>( cfg, backend_, localRpcEngine, etl, dosguard ); static constexpr auto kREQUEST = R"JSON({ "command": "server_info", "id": 99 })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kRESPONSE = R"JSON({ "error": "tooBusy", "error_code": 9, "error_message": "The server is too busy to help you now.", "status": "error", "type": "response" })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(kREQUEST).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*localRpcEngine, notifyTooBusy).Times(1); EXPECT_CALL(*localRpcEngine, post).WillOnce(testing::Return(false)); (*localHandler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, HTTPTooBusy) { auto localRpcEngine = std::make_shared(); auto localHandler = std::make_shared>( cfg, backend_, localRpcEngine, etl, dosguard ); static constexpr auto kREQUEST = R"JSON({ "method": "server_info", "params": [{}] })JSON"; backend_->setRange(kMIN_SEQ, kMAX_SEQ); static constexpr auto kRESPONSE = R"JSON({ "error": "tooBusy", "error_code": 9, "error_message": "The server is too busy to help you now.", "status": "error", "type": "response" })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(kREQUEST).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*localRpcEngine, notifyTooBusy).Times(1); EXPECT_CALL(*localRpcEngine, post).WillOnce(testing::Return(false)); (*localHandler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } TEST_F(WebRPCServerHandlerTest, HTTPRequestNotJson) { static constexpr auto kREQUEST = "not json"; static constexpr auto kRESPONSE_PREFIX = "Unable to parse JSON from the request"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); (*handler)(kREQUEST, session); EXPECT_THAT(session->message, testing::StartsWith(kRESPONSE_PREFIX)); EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); } TEST_F(WebRPCServerHandlerTest, WsRequestNotJson) { session->upgraded = true; static constexpr auto kREQUEST = "not json"; static constexpr auto kRESPONSE = R"JSON({ "error": "badSyntax", "error_code": 1, "error_message": "Syntax error.", "status": "error", "type": "response" })JSON"; EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); (*handler)(kREQUEST, session); EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(kRESPONSE)); } struct InvalidAPIVersionTestBundle { std::string testName; std::string version; std::string wsMessage; }; // parameterized test cases for parameters check struct WebRPCServerHandlerInvalidAPIVersionParamTest : public WebRPCServerHandlerTest, public testing::WithParamInterface {}; auto generateInvalidVersions() { return std::vector{ {.testName = "v0", .version = "0", .wsMessage = fmt::format( "Requested API version is lower than minimum supported ({})", rpc::kAPI_VERSION_MIN )}, {.testName = "v4", .version = "4", .wsMessage = fmt::format( "Requested API version is higher than maximum supported ({})", rpc::kAPI_VERSION_MAX )}, {.testName = "null", .version = "null", .wsMessage = "API version must be an integer"}, {.testName = "str", .version = "\"bogus\"", .wsMessage = "API version must be an integer"}, {.testName = "bool", .version = "false", .wsMessage = "API version must be an integer"}, {.testName = "double", .version = "12.34", .wsMessage = "API version must be an integer"}, }; } INSTANTIATE_TEST_CASE_P( WebRPCServerHandlerAPIVersionGroup, WebRPCServerHandlerInvalidAPIVersionParamTest, testing::ValuesIn(generateInvalidVersions()), tests::util::kNAME_GENERATOR ); TEST_P(WebRPCServerHandlerInvalidAPIVersionParamTest, HTTPInvalidAPIVersion) { auto request = fmt::format( R"JSON({{ "method": "server_info", "params": [{{ "api_version": {} }}] }})JSON", GetParam().version ); backend_->setRange(kMIN_SEQ, kMAX_SEQ); EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(request).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); (*handler)(request, session); EXPECT_EQ(session->message, "invalid_API_version"); EXPECT_EQ(session->lastStatus, boost::beast::http::status::bad_request); } TEST_P(WebRPCServerHandlerInvalidAPIVersionParamTest, WSInvalidAPIVersion) { session->upgraded = true; auto request = fmt::format( R"JSON({{ "method": "server_info", "api_version": {} }})JSON", GetParam().version ); backend_->setRange(kMIN_SEQ, kMAX_SEQ); EXPECT_CALL(dosguard, isOk(session->clientIp())).WillOnce(testing::Return(true)); EXPECT_CALL(dosguard, request(session->clientIp(), boost::json::parse(request).as_object())) .WillOnce(testing::Return(true)); EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1); (*handler)(request, session); auto response = boost::json::parse(session->message); EXPECT_TRUE(response.is_object()); EXPECT_TRUE(response.as_object().contains("error")); EXPECT_EQ(response.at("error").as_string(), "invalid_API_version"); EXPECT_TRUE(response.as_object().contains("error_code")); EXPECT_EQ( response.at("error_code").as_int64(), static_cast(rpc::ClioError::RpcInvalidApiVersion) ); EXPECT_TRUE(response.as_object().contains("error_message")); EXPECT_EQ(response.at("error_message").as_string(), GetParam().wsMessage); }