Files
clio/tests/unit/rpc/handlers/ServerInfoTests.cpp
2026-05-13 12:09:06 +01:00

543 lines
20 KiB
C++

#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 <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/value_to.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
#include <optional>
#include <string>
using namespace rpc;
using namespace data;
namespace json = boost::json;
using namespace testing;
using TestServerInfoHandler = BaseServerInfoHandler<MockCounters>;
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<std::string>(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<std::string>(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<std::string>(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