#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/Spawn.hpp" #include "util/Taggable.hpp" #include "util/config/ConfigDefinition.hpp" #include "util/config/ConfigValue.hpp" #include "util/config/Types.hpp" #include "web/SubscriptionContextInterface.hpp" #include "web/dosguard/DOSGuardMock.hpp" #include "web/ng/MockConnection.hpp" #include "web/ng/RPCServerHandler.hpp" #include "web/ng/Request.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace web::ng; using testing::Return; using testing::StrictMock; using namespace util::config; namespace http = boost::beast::http; struct NgRpcServerHandlerTest : util::prometheus::WithPrometheus, MockBackendTestStrict, SyncAsioContextTest { ClioConfigDefinition config{ClioConfigDefinition{ {"log.tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}, {"api_version.min", ConfigValue{ConfigType::Integer}.defaultValue(1)}, {"api_version.max", ConfigValue{ConfigType::Integer}.defaultValue(2)}, {"api_version.default", ConfigValue{ConfigType::Integer}.defaultValue(1)} }}; protected: std::shared_ptr> rpcEngine_ = std::make_shared>(); std::shared_ptr> etl_ = std::make_shared>(); DOSGuardStrictMock dosguard_; RPCServerHandler rpcServerHandler_{config, backend_, rpcEngine_, etl_, dosguard_}; util::TagDecoratorFactory tagFactory_{config}; std::string const ip_ = "some ip"; StrictMockConnectionMetadata connectionMetadata_{ip_, tagFactory_}; Request::HttpHeaders const httpHeaders_; static Request makeHttpRequest(std::string_view body) { return Request{http::request{http::verb::post, "/", 11, body}}; } Request makeWsRequest(std::string body) { return Request{std::move(body), httpHeaders_}; } }; TEST_F(NgRpcServerHandlerTest, DosguardRejectedHttpRequest) { runSpawn([&](boost::asio::yield_context yield) { auto const request = makeHttpRequest("some message"); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(false)); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); auto const responseHttp = std::move(response).intoHttpResponse(); EXPECT_EQ(responseHttp.result(), http::status::service_unavailable); auto const responseJson = boost::json::parse(responseHttp.body()).as_object(); EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN); }); } TEST_F(NgRpcServerHandlerTest, DosguardRejectedWsRequest) { runSpawn([&](boost::asio::yield_context yield) { auto const requestStr = "some message"; auto const request = makeWsRequest(requestStr); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(false)); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); auto const responseWs = boost::beast::buffers_to_string(response.asWsResponse()); auto const responseJson = boost::json::parse(responseWs).as_object(); EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN); EXPECT_EQ(responseJson.at("request").as_string(), requestStr); }); } TEST_F(NgRpcServerHandlerTest, DosguardRejectedWsJsonRequest) { runSpawn([&](boost::asio::yield_context yield) { auto const requestStr = R"JSON({"request": "some message", "id": "some id"})JSON"; auto const request = makeWsRequest(requestStr); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(false)); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); auto const responseWs = boost::beast::buffers_to_string(response.asWsResponse()); auto const responseJson = boost::json::parse(responseWs).as_object(); EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN); EXPECT_EQ(responseJson.at("request").as_string(), requestStr); EXPECT_EQ(responseJson.at("id").as_string(), "some id"); }); } TEST_F(NgRpcServerHandlerTest, PostToRpcEngineFailed) { runSpawn([&](boost::asio::yield_context yield) { auto const request = makeHttpRequest("some message"); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(*rpcEngine_, post).WillOnce(Return(false)); EXPECT_CALL(*rpcEngine_, notifyTooBusy()); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); EXPECT_EQ( std::move(response).intoHttpResponse().result(), http::status::service_unavailable ); }); } TEST_F(NgRpcServerHandlerTest, CoroutineSleepsUntilRpcEngineFinishes) { StrictMock> rpcServerHandlerDone; StrictMock> rpcEngineDone; testing::Expectation const expectedRpcEngineDone = EXPECT_CALL(rpcEngineDone, Call); EXPECT_CALL(rpcServerHandlerDone, Call).After(expectedRpcEngineDone); runSpawn([&](boost::asio::yield_context yield) { auto const request = makeHttpRequest("some message"); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true)); EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) { util::spawn( ctx_, [this, &rpcEngineDone, fn = std::forward(fn)](boost::asio::yield_context yield) { EXPECT_CALL(*rpcEngine_, notifyBadSyntax); fn(yield); rpcEngineDone.Call(); } ); return true; }); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); rpcServerHandlerDone.Call(); EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request); }); } TEST_F(NgRpcServerHandlerTest, JsonParseFailed) { runSpawn([&](boost::asio::yield_context yield) { auto const request = makeHttpRequest("not a json"); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true)); EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) { EXPECT_CALL(*rpcEngine_, notifyBadSyntax); fn(yield); return true; }); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request); }); } TEST_F(NgRpcServerHandlerTest, DosguardRejectedParsedRequest) { runSpawn([&](boost::asio::yield_context yield) { std::string const requestStr = "{}"; auto const request = makeHttpRequest(requestStr); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())) .WillOnce(Return(false)); EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) { fn(yield); return true; }); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true)); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); auto const responseHttp = std::move(response).intoHttpResponse(); EXPECT_EQ(responseHttp.result(), http::status::service_unavailable); auto const responseJson = boost::json::parse(responseHttp.body()).as_object(); EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN); }); } TEST_F(NgRpcServerHandlerTest, DosguardAddsLoadWarning) { runSpawn([&](boost::asio::yield_context yield) { std::string const requestStr = "{}"; auto const request = makeHttpRequest(requestStr); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())) .WillOnce(Return(false)); EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) { fn(yield); return true; }); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(false)); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); auto const responseHttp = std::move(response).intoHttpResponse(); EXPECT_EQ(responseHttp.result(), http::status::service_unavailable); auto const responseJson = boost::json::parse(responseHttp.body()).as_object(); EXPECT_EQ(responseJson.at("error_code").as_int64(), rpc::RippledError::rpcSLOW_DOWN); EXPECT_EQ(responseJson.at("warning").as_string(), "load"); EXPECT_EQ( responseJson.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcRateLimit ); }); } TEST_F(NgRpcServerHandlerTest, GotNotJsonObject) { runSpawn([&](boost::asio::yield_context yield) { auto const request = makeHttpRequest("[]"); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true)); EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) { EXPECT_CALL(*rpcEngine_, notifyBadSyntax); fn(yield); return true; }); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request); }); } TEST_F(NgRpcServerHandlerTest, HandleRequest_NoRangeFromBackend) { runSpawn([&](boost::asio::yield_context yield) { std::string const requestStr = "{}"; auto const request = makeHttpRequest(requestStr); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())) .WillOnce(Return(true)); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true)); EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) { EXPECT_CALL(connectionMetadata_, wasUpgraded).WillOnce(Return(not request.isHttp())); EXPECT_CALL(*rpcEngine_, notifyNotReady); fn(yield); return true; }); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); auto const httpResponse = std::move(response).intoHttpResponse(); EXPECT_EQ(httpResponse.result(), http::status::ok); auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object(); EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "notReady"); }); } TEST_F(NgRpcServerHandlerTest, HandleRequest_ContextCreationFailed) { backend_->setRange(0, 1); runSpawn([&](boost::asio::yield_context yield) { std::string const requestStr = "{}"; auto const request = makeHttpRequest(requestStr); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())) .WillOnce(Return(true)); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true)); EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) { EXPECT_CALL(connectionMetadata_, wasUpgraded) .WillRepeatedly(Return(not request.isHttp())); EXPECT_CALL(*rpcEngine_, notifyBadSyntax); fn(yield); return true; }); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); auto const httpResponse = std::move(response).intoHttpResponse(); EXPECT_EQ(httpResponse.result(), http::status::bad_request); EXPECT_EQ(httpResponse.body(), "Null method"); }); } TEST_F(NgRpcServerHandlerTest, HandleRequest_BuildResponseFailed) { backend_->setRange(0, 1); runSpawn([&](boost::asio::yield_context yield) { std::string const requestStr = R"JSON({"method": "some_method"})JSON"; auto const request = makeHttpRequest(requestStr); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())) .WillOnce(Return(true)); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true)); EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) { EXPECT_CALL(connectionMetadata_, wasUpgraded) .WillRepeatedly(Return(not request.isHttp())); EXPECT_CALL(*rpcEngine_, buildResponse) .WillOnce(Return(rpc::Result{rpc::Status{rpc::ClioError::RpcUnknownOption}})); EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1)); fn(yield); return true; }); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); auto const httpResponse = std::move(response).intoHttpResponse(); EXPECT_EQ(httpResponse.result(), http::status::ok); auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object(); EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "unknownOption"); ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1); EXPECT_EQ( jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio ); }); } TEST_F(NgRpcServerHandlerTest, HandleRequest_BuildResponseThrewAnException) { backend_->setRange(0, 1); runSpawn([&](boost::asio::yield_context yield) { std::string const requestStr = R"JSON({"method": "some_method"})JSON"; auto const request = makeHttpRequest(requestStr); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())) .WillOnce(Return(true)); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true)); EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) { EXPECT_CALL(connectionMetadata_, wasUpgraded) .WillRepeatedly(Return(not request.isHttp())); EXPECT_CALL(*rpcEngine_, buildResponse).WillOnce([](auto&&) -> rpc::Result { throw std::runtime_error("some error"); }); EXPECT_CALL(*rpcEngine_, notifyInternalError); fn(yield); return true; }); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); auto const httpResponse = std::move(response).intoHttpResponse(); EXPECT_EQ(httpResponse.result(), http::status::internal_server_error); }); } TEST_F(NgRpcServerHandlerTest, HandleRequest_Successful_HttpRequest) { backend_->setRange(0, 1); runSpawn([&](boost::asio::yield_context yield) { std::string const requestStr = R"JSON({"method": "some_method"})JSON"; auto const request = makeHttpRequest(requestStr); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())) .WillOnce(Return(true)); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true)); EXPECT_CALL(*rpcEngine_, recordLedgerMetrics); EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) { EXPECT_CALL(connectionMetadata_, wasUpgraded) .WillRepeatedly(Return(not request.isHttp())); EXPECT_CALL(*rpcEngine_, buildResponse) .WillOnce(Return( rpc::Result{rpc::ReturnType{boost::json::object{{"some key", "some value"}}}} )); EXPECT_CALL(*rpcEngine_, notifyComplete); EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1)); fn(yield); return true; }); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); auto const httpResponse = std::move(response).intoHttpResponse(); EXPECT_EQ(httpResponse.result(), http::status::ok); auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object(); EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value"); EXPECT_EQ(jsonResponse.at("result").at("status").as_string(), "success"); ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse; EXPECT_EQ( jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio ); }); } TEST_F(NgRpcServerHandlerTest, HandleRequest_OutdatedWarning) { backend_->setRange(0, 1); runSpawn([&](boost::asio::yield_context yield) { std::string const requestStr = R"JSON({"method": "some_method"})JSON"; auto const request = makeHttpRequest(requestStr); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())) .WillOnce(Return(true)); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true)); EXPECT_CALL(*rpcEngine_, recordLedgerMetrics); 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 warningCodes; std::ranges::transform( jsonResponse.at("warnings").as_array(), std::inserter(warningCodes, warningCodes.end()), [](auto const& w) { return w.as_object().at("id").as_int64(); } ); EXPECT_EQ(warningCodes.size(), 2); EXPECT_TRUE(warningCodes.contains(rpc::WarnRpcClio)); EXPECT_TRUE(warningCodes.contains(rpc::WarnRpcOutdated)); }); } TEST_F(NgRpcServerHandlerTest, HandleRequest_Successful_HttpRequest_Forwarded) { backend_->setRange(0, 1); runSpawn([&](boost::asio::yield_context yield) { std::string const requestStr = R"JSON({"method": "some_method"})JSON"; auto const request = makeHttpRequest(requestStr); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())) .WillOnce(Return(true)); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true)); EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) { EXPECT_CALL(connectionMetadata_, wasUpgraded) .WillRepeatedly(Return(not request.isHttp())); EXPECT_CALL(*rpcEngine_, buildResponse) .WillOnce(Return( rpc::Result{rpc::ReturnType{boost::json::object{ {"result", boost::json::object{{"some key", "some value"}}}, {"forwarded", true} }}} )); EXPECT_CALL(*rpcEngine_, notifyComplete); EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1)); fn(yield); return true; }); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); auto const httpResponse = std::move(response).intoHttpResponse(); EXPECT_EQ(httpResponse.result(), http::status::ok); auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object(); EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value"); EXPECT_EQ(jsonResponse.at("result").at("status").as_string(), "success"); EXPECT_EQ(jsonResponse.at("forwarded").as_bool(), true); ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse; EXPECT_EQ( jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio ); }); } TEST_F(NgRpcServerHandlerTest, HandleRequest_Successful_HttpRequest_HasError) { backend_->setRange(0, 1); runSpawn([&](boost::asio::yield_context yield) { std::string const requestStr = R"JSON({"method": "some_method"})JSON"; auto const request = makeHttpRequest(requestStr); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())) .WillOnce(Return(true)); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true)); EXPECT_CALL(*rpcEngine_, recordLedgerMetrics); EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) { EXPECT_CALL(connectionMetadata_, wasUpgraded) .WillRepeatedly(Return(not request.isHttp())); EXPECT_CALL(*rpcEngine_, buildResponse) .WillOnce(Return( rpc::Result{rpc::ReturnType{ boost::json::object{{"some key", "some value"}, {"error", "some error"}} }} )); EXPECT_CALL(*rpcEngine_, notifyComplete); EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1)); fn(yield); return true; }); auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield); auto const httpResponse = std::move(response).intoHttpResponse(); EXPECT_EQ(httpResponse.result(), http::status::ok); auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object(); EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value"); EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "some error"); ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse; EXPECT_EQ( jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio ); }); } struct NgRpcServerHandlerWsTest : NgRpcServerHandlerTest { struct MockSubscriptionContext : web::SubscriptionContextInterface { using web::SubscriptionContextInterface::SubscriptionContextInterface; MOCK_METHOD(void, send, (std::shared_ptr), (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; protected: std::shared_ptr subscriptionContext_ = std::make_shared(tagFactory_); }; TEST_F(NgRpcServerHandlerWsTest, HandleRequest_Successful_WsRequest) { backend_->setRange(0, 1); runSpawn([&](boost::asio::yield_context yield) { Request::HttpHeaders const headers; std::string const requestStr = R"JSON({"method": "some_method", "id": 1234, "api_version": 1})JSON"; auto const request = Request(requestStr, headers); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())) .WillOnce(Return(true)); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true)); EXPECT_CALL(*rpcEngine_, recordLedgerMetrics); EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) { EXPECT_CALL(connectionMetadata_, wasUpgraded) .WillRepeatedly(Return(not request.isHttp())); EXPECT_CALL(*rpcEngine_, buildResponse) .WillOnce(Return( rpc::Result{rpc::ReturnType{boost::json::object{{"some key", "some value"}}}} )); EXPECT_CALL(*rpcEngine_, notifyComplete); EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1)); fn(yield); return true; }); auto const response = rpcServerHandler_(request, connectionMetadata_, subscriptionContext_, yield); auto const jsonResponse = boost::json::parse(response.message()).as_object(); EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value"); EXPECT_EQ(jsonResponse.at("status").as_string(), "success"); EXPECT_EQ(jsonResponse.at("type").as_string(), "response"); EXPECT_EQ(jsonResponse.at("id").as_int64(), 1234); EXPECT_EQ(jsonResponse.at("api_version").as_int64(), 1); ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse; EXPECT_EQ( jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio ); }); } TEST_F(NgRpcServerHandlerWsTest, HandleRequest_Successful_WsRequest_HasError) { backend_->setRange(0, 1); runSpawn([&](boost::asio::yield_context yield) { Request::HttpHeaders const headers; std::string const requestStr = R"JSON({"method": "some_method", "id": 1234, "api_version": 1})JSON"; auto const request = Request(requestStr, headers); EXPECT_CALL(dosguard_, isOk(ip_)).WillOnce(Return(true)); EXPECT_CALL(dosguard_, request(ip_, boost::json::parse(requestStr).as_object())) .WillOnce(Return(true)); EXPECT_CALL(dosguard_, add(ip_, testing::_)).WillOnce(Return(true)); EXPECT_CALL(*rpcEngine_, recordLedgerMetrics); EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) { EXPECT_CALL(connectionMetadata_, wasUpgraded) .WillRepeatedly(Return(not request.isHttp())); EXPECT_CALL(*rpcEngine_, buildResponse) .WillOnce(Return( rpc::Result{rpc::ReturnType{ boost::json::object{{"some key", "some value"}, {"error", "some error"}} }} )); EXPECT_CALL(*rpcEngine_, notifyComplete); EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1)); fn(yield); return true; }); auto const response = rpcServerHandler_(request, connectionMetadata_, subscriptionContext_, yield); auto const jsonResponse = boost::json::parse(response.message()).as_object(); EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value"); EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "some error"); EXPECT_EQ(jsonResponse.at("type").as_string(), "response"); EXPECT_EQ(jsonResponse.at("id").as_int64(), 1234); EXPECT_EQ(jsonResponse.at("api_version").as_int64(), 1); ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse; EXPECT_EQ( jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::WarnRpcClio ); }); }