#include "rpc/Errors.hpp" #include "rpc/common/AnyHandler.hpp" #include "rpc/common/Types.hpp" #include "rpc/handlers/ServerInfo.hpp" #include "util/HandlerBaseTestFixture.hpp" #include "util/MockCounters.hpp" #include "util/MockCountersFixture.hpp" #include "util/MockETLService.hpp" #include "util/MockETLServiceTestFixture.hpp" #include "util/MockLoadBalancer.hpp" #include "util/MockSubscriptionManager.hpp" #include "util/TestObject.hpp" #include #include #include #include #include #include #include #include #include using namespace rpc; using namespace data; namespace json = boost::json; using namespace testing; using TestServerInfoHandler = BaseServerInfoHandler; namespace { constexpr auto kLedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; constexpr auto kClientIp = "1.1.1.1"; } // namespace struct RPCServerInfoHandlerTest : HandlerBaseTest, MockLoadBalancerTest, MockCountersTest { RPCServerInfoHandlerTest() { backend_->setRange(10, 30); } static void validateNormalOutput(rpc::ReturnType const& output) { ASSERT_TRUE(output); auto const& result = output.result.value().as_object(); EXPECT_TRUE(result.contains("info")); auto const& info = result.at("info").as_object(); EXPECT_TRUE(info.contains("complete_ledgers")); EXPECT_EQ(boost::json::value_to(info.at("complete_ledgers")), "10-30"); EXPECT_TRUE(info.contains("load_factor")); EXPECT_TRUE(info.contains("clio_version")); EXPECT_TRUE(info.contains("libxrpl_version")); EXPECT_TRUE(info.contains("validated_ledger")); EXPECT_TRUE(info.contains("time")); EXPECT_TRUE(info.contains("uptime")); auto const& validated = info.at("validated_ledger").as_object(); EXPECT_TRUE(validated.contains("age")); EXPECT_EQ(validated.at("age").as_uint64(), 3u); EXPECT_TRUE(validated.contains("hash")); EXPECT_EQ(boost::json::value_to(validated.at("hash")), kLedgerHash); EXPECT_TRUE(validated.contains("seq")); EXPECT_EQ(validated.at("seq").as_uint64(), 30u); EXPECT_TRUE(validated.contains("base_fee_xrp")); EXPECT_EQ(validated.at("base_fee_xrp").as_double(), 1e-06); EXPECT_TRUE(validated.contains("reserve_base_xrp")); EXPECT_EQ(validated.at("reserve_base_xrp").as_double(), 3e-06); EXPECT_TRUE(validated.contains("reserve_inc_xrp")); EXPECT_EQ(validated.at("reserve_inc_xrp").as_double(), 2e-06); auto const& cache = info.at("cache").as_object(); EXPECT_TRUE(cache.contains("size")); EXPECT_TRUE(cache.contains("is_full")); EXPECT_TRUE(cache.contains("latest_ledger_seq")); EXPECT_TRUE(cache.contains("object_hit_rate")); EXPECT_TRUE(cache.contains("successor_hit_rate")); EXPECT_TRUE(cache.contains("is_enabled")); } static void validateAdminOutput(rpc::ReturnType const& output, bool shouldHaveBackendCounters = false) { auto const& result = output.result.value().as_object(); auto const& info = result.at("info").as_object(); EXPECT_TRUE(info.contains("etl")); EXPECT_TRUE(info.contains("counters")); if (shouldHaveBackendCounters) { ASSERT_TRUE(info.contains("backend_counters")) << boost::json::serialize(info); EXPECT_TRUE(info.at("backend_counters").is_object()); EXPECT_TRUE(!info.at("backend_counters").as_object().empty()); } } static void validateRippledOutput(rpc::ReturnType const& output) { auto const& result = output.result.value().as_object(); auto const& info = result.at("info").as_object(); EXPECT_TRUE(info.contains("load_factor")); EXPECT_EQ(info.at("load_factor").as_int64(), 234); EXPECT_TRUE(info.contains("validation_quorum")); EXPECT_EQ(info.at("validation_quorum").as_int64(), 456); EXPECT_TRUE(info.contains("rippled_version")); EXPECT_EQ(boost::json::value_to(info.at("rippled_version")), "1234"); EXPECT_TRUE(info.contains("network_id")); EXPECT_EQ(info.at("network_id").as_int64(), 2); } protected: StrictMockSubscriptionManagerSharedPtr mockSubscriptionManagerPtr_; }; TEST_F(RPCServerInfoHandlerTest, NoLedgerHeaderErrorsOutWithInternal) { EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(std::nullopt)); auto const handler = AnyHandler{TestServerInfoHandler{ backend_, mockSubscriptionManagerPtr_, mockLoadBalancerPtr_, mockETLServicePtr_, *mockCountersPtr_ }}; runSpawn([&](auto yield) { auto const req = json::parse("{}"); auto const output = handler.process(req, Context{yield}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), "internal"); EXPECT_EQ(err.at("error_message").as_string(), "Internal error."); }); } TEST_F(RPCServerInfoHandlerTest, NoFeesErrorsOutWithInternal) { auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30); EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(std::nullopt)); auto const handler = AnyHandler{TestServerInfoHandler{ backend_, mockSubscriptionManagerPtr_, mockLoadBalancerPtr_, mockETLServicePtr_, *mockCountersPtr_ }}; runSpawn([&](auto yield) { auto const req = json::parse("{}"); auto const output = handler.process(req, Context{yield}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), "internal"); EXPECT_EQ(err.at("error_message").as_string(), "Internal error."); }); } TEST_F(RPCServerInfoHandlerTest, DefaultOutputIsPresent) { MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get(); MockETLService const* rawETLServicePtr = mockETLServicePtr_.get(); auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0); EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(feeBlob)); EXPECT_CALL( *rawBalancerPtr, forwardToRippled(testing::_, testing::Eq(kClientIp), false, testing::_) ) .WillOnce(Return(std::unexpected{rpc::ClioError::EtlInvalidResponse})); EXPECT_CALL(*rawCountersPtr, uptime).WillOnce(Return(std::chrono::seconds{1234})); EXPECT_CALL(*rawETLServicePtr, isAmendmentBlocked).WillOnce(Return(false)); auto const handler = AnyHandler{TestServerInfoHandler{ backend_, mockSubscriptionManagerPtr_, mockLoadBalancerPtr_, mockETLServicePtr_, *mockCountersPtr_ }}; runSpawn([&](auto yield) { auto const req = json::parse("{}"); auto const output = handler.process(req, Context{yield, {}, false, kClientIp}); validateNormalOutput(output); // no admin section present by default auto const& result = output.result.value().as_object(); auto const& info = result.at("info").as_object(); EXPECT_FALSE(info.contains("etl")); EXPECT_FALSE(info.contains("counters")); }); } TEST_F(RPCServerInfoHandlerTest, AmendmentBlockedIsPresentIfSet) { MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get(); MockETLService const* rawETLServicePtr = mockETLServicePtr_.get(); auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0); EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(feeBlob)); EXPECT_CALL( *rawBalancerPtr, forwardToRippled(testing::_, testing::Eq(kClientIp), false, testing::_) ) .WillOnce(Return(std::unexpected{rpc::ClioError::EtlInvalidResponse})); EXPECT_CALL(*rawCountersPtr, uptime).WillOnce(Return(std::chrono::seconds{1234})); EXPECT_CALL(*rawETLServicePtr, isAmendmentBlocked).WillOnce(Return(true)); auto const handler = AnyHandler{TestServerInfoHandler{ backend_, mockSubscriptionManagerPtr_, mockLoadBalancerPtr_, mockETLServicePtr_, *mockCountersPtr_ }}; runSpawn([&](auto yield) { auto const req = json::parse("{}"); auto const output = handler.process(req, Context{yield, {}, false, kClientIp}); validateNormalOutput(output); auto const& info = output.result.value().as_object().at("info").as_object(); EXPECT_TRUE(info.contains("amendment_blocked")); EXPECT_EQ(info.at("amendment_blocked").as_bool(), true); }); } TEST_F(RPCServerInfoHandlerTest, CorruptionDetectedIsPresentIfSet) { MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get(); MockETLService const* rawETLServicePtr = mockETLServicePtr_.get(); auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0); EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(feeBlob)); EXPECT_CALL( *rawBalancerPtr, forwardToRippled(testing::_, testing::Eq(kClientIp), false, testing::_) ) .WillOnce(Return(std::unexpected{rpc::ClioError::EtlInvalidResponse})); EXPECT_CALL(*rawCountersPtr, uptime).WillOnce(Return(std::chrono::seconds{1234})); EXPECT_CALL(*rawETLServicePtr, isCorruptionDetected).WillOnce(Return(true)); auto const handler = AnyHandler{TestServerInfoHandler{ backend_, mockSubscriptionManagerPtr_, mockLoadBalancerPtr_, mockETLServicePtr_, *mockCountersPtr_ }}; runSpawn([&](auto yield) { auto const req = json::parse("{}"); auto const output = handler.process(req, Context{yield, {}, false, kClientIp}); validateNormalOutput(output); auto const& info = output.result.value().as_object().at("info").as_object(); EXPECT_TRUE(info.contains("corruption_detected")); EXPECT_EQ(info.at("corruption_detected").as_bool(), true); }); } TEST_F(RPCServerInfoHandlerTest, CacheReportsEnabledFlagCorrectly) { MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get(); auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(2).WillRepeatedly(Return(ledgerHeader)); auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0); EXPECT_CALL(*backend_, doFetchLedgerObject).Times(2).WillRepeatedly(Return(feeBlob)); EXPECT_CALL( *rawBalancerPtr, forwardToRippled(testing::_, testing::Eq(kClientIp), false, testing::_) ) .Times(2) .WillRepeatedly(Return(std::unexpected{rpc::ClioError::EtlInvalidResponse})); EXPECT_CALL(*rawCountersPtr, uptime) .Times(2) .WillRepeatedly(Return(std::chrono::seconds{1234})); auto const handler = AnyHandler{TestServerInfoHandler{ backend_, mockSubscriptionManagerPtr_, mockLoadBalancerPtr_, mockETLServicePtr_, *mockCountersPtr_ }}; runSpawn([&](auto yield) { auto const req = json::parse("{}"); auto const output = handler.process(req, Context{yield, {}, false, kClientIp}); validateNormalOutput(output); auto const& cache = output.result.value().as_object().at("info").as_object().at("cache").as_object(); EXPECT_TRUE(cache.contains("is_enabled")); EXPECT_EQ(cache.at("is_enabled").as_bool(), true); }); backend_->cache().setDisabled(); runSpawn([&](auto yield) { auto const req = json::parse("{}"); auto const output = handler.process(req, Context{yield, {}, false, kClientIp}); validateNormalOutput(output); auto const& cache = output.result.value().as_object().at("info").as_object().at("cache").as_object(); EXPECT_TRUE(cache.contains("is_enabled")); EXPECT_EQ(cache.at("is_enabled").as_bool(), false); }); } TEST_F(RPCServerInfoHandlerTest, AdminSectionPresentWhenAdminFlagIsSet) { MockLoadBalancer const* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get(); MockETLService const* rawETLServicePtr = mockETLServicePtr_.get(); auto const empty = json::object{}; auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0); EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(feeBlob)); EXPECT_CALL(*rawBalancerPtr, forwardToRippled).WillOnce(Return(empty)); EXPECT_CALL(*rawCountersPtr, uptime).WillOnce(Return(std::chrono::seconds{1234})); EXPECT_CALL(*rawETLServicePtr, isAmendmentBlocked).WillOnce(Return(false)); // admin calls EXPECT_CALL(*rawCountersPtr, report).WillOnce(Return(empty)); EXPECT_CALL(*mockSubscriptionManagerPtr_, report).WillOnce(Return(empty)); EXPECT_CALL(*rawETLServicePtr, getInfo).WillOnce(Return(empty)); auto const handler = AnyHandler{TestServerInfoHandler{ backend_, mockSubscriptionManagerPtr_, mockLoadBalancerPtr_, mockETLServicePtr_, *mockCountersPtr_ }}; runSpawn([&](auto yield) { auto const req = json::parse("{}"); auto const output = handler.process(req, Context{yield, {}, true}); validateNormalOutput(output); validateAdminOutput(output); }); } TEST_F(RPCServerInfoHandlerTest, BackendCountersPresentWhenRequestWithParam) { MockLoadBalancer const* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get(); MockETLService const* rawETLServicePtr = mockETLServicePtr_.get(); auto const empty = json::object{}; auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0); EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(feeBlob)); EXPECT_CALL(*rawBalancerPtr, forwardToRippled).WillOnce(Return(empty)); EXPECT_CALL(*rawCountersPtr, uptime).WillOnce(Return(std::chrono::seconds{1234})); EXPECT_CALL(*rawETLServicePtr, isAmendmentBlocked).WillOnce(Return(false)); // admin calls EXPECT_CALL(*rawCountersPtr, report).WillOnce(Return(empty)); EXPECT_CALL(*mockSubscriptionManagerPtr_, report).WillOnce(Return(empty)); EXPECT_CALL(*rawETLServicePtr, getInfo).WillOnce(Return(empty)); EXPECT_CALL(*backend_, stats) .WillOnce(Return(boost::json::object{{"read_cout", 10}, {"write_count", 3}})); auto const handler = AnyHandler{TestServerInfoHandler{ backend_, mockSubscriptionManagerPtr_, mockLoadBalancerPtr_, mockETLServicePtr_, *mockCountersPtr_ }}; runSpawn([&](auto yield) { auto const req = json::parse(R"JSON( { "backend_counters": true } )JSON"); auto const output = handler.process(req, Context{yield, {}, true}); validateNormalOutput(output); validateAdminOutput(output, true); }); } TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesPresent) { MockLoadBalancer const* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get(); MockETLService const* rawETLServicePtr = mockETLServicePtr_.get(); auto const empty = json::object{}; auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0); EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(feeBlob)); EXPECT_CALL(*rawCountersPtr, uptime).WillOnce(Return(std::chrono::seconds{1234})); EXPECT_CALL(*rawETLServicePtr, isAmendmentBlocked).WillOnce(Return(false)); auto const rippledObj = json::parse(R"JSON({ "result": { "info": { "build_version": "1234", "validation_quorum": 456, "load_factor": 234, "network_id": 2 } } })JSON"); EXPECT_CALL(*rawBalancerPtr, forwardToRippled).WillOnce(Return(rippledObj.as_object())); // admin calls EXPECT_CALL(*rawCountersPtr, report).WillOnce(Return(empty)); EXPECT_CALL(*mockSubscriptionManagerPtr_, report).WillOnce(Return(empty)); EXPECT_CALL(*rawETLServicePtr, getInfo).WillOnce(Return(empty)); auto const handler = AnyHandler{TestServerInfoHandler{ backend_, mockSubscriptionManagerPtr_, mockLoadBalancerPtr_, mockETLServicePtr_, *mockCountersPtr_ }}; runSpawn([&](auto yield) { auto const req = json::parse("{}"); auto const output = handler.process(req, Context{yield, {}, true}); validateNormalOutput(output); validateAdminOutput(output); validateRippledOutput(output); }); } TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesMissingNoExceptionThrown) { MockLoadBalancer const* rawBalancerPtr = mockLoadBalancerPtr_.get(); MockCounters const* rawCountersPtr = mockCountersPtr_.get(); MockETLService const* rawETLServicePtr = mockETLServicePtr_.get(); auto const empty = json::object{}; auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader)); auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0); EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(feeBlob)); EXPECT_CALL(*rawCountersPtr, uptime).WillOnce(Return(std::chrono::seconds{1234})); EXPECT_CALL(*rawETLServicePtr, isAmendmentBlocked).WillOnce(Return(false)); auto const rippledObj = json::parse(R"JSON({ "result": { "info": {} } })JSON"); EXPECT_CALL(*rawBalancerPtr, forwardToRippled).WillOnce(Return(rippledObj.as_object())); // admin calls EXPECT_CALL(*rawCountersPtr, report).WillOnce(Return(empty)); EXPECT_CALL(*mockSubscriptionManagerPtr_, report).WillOnce(Return(empty)); EXPECT_CALL(*rawETLServicePtr, getInfo).WillOnce(Return(empty)); auto const handler = AnyHandler{TestServerInfoHandler{ backend_, mockSubscriptionManagerPtr_, mockLoadBalancerPtr_, mockETLServicePtr_, *mockCountersPtr_ }}; runSpawn([&](auto yield) { auto const req = json::parse("{}"); auto const output = handler.process(req, Context{yield, {}, true}); validateNormalOutput(output); validateAdminOutput(output); }); } // TODO: In the future we'd like to refactor to add mock and test for cache