Files
clio/tests/unit/rpc/handlers/FeatureTests.cpp
github-actions[bot] 0beaff15cd style: clang-tidy auto fixes (#3015)
Fixes #3014. Please review and commit clang-tidy fixes.

Co-authored-by: godexsoft <385326+godexsoft@users.noreply.github.com>
2026-03-25 10:27:59 +00:00

547 lines
20 KiB
C++

#include "data/AmendmentCenter.hpp"
#include "data/Types.hpp"
#include "rpc/Errors.hpp"
#include "rpc/common/AnyHandler.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/handlers/Feature.hpp"
#include "util/HandlerBaseTestFixture.hpp"
#include "util/MockAmendmentCenter.hpp"
#include "util/NameGenerator.hpp"
#include "util/TestObject.hpp"
#include <boost/json/parse.hpp>
#include <fmt/format.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/base_uint.h>
#include <optional>
#include <string>
#include <vector>
using namespace rpc;
using namespace data;
namespace {
constexpr auto kRANGE_MIN = 10;
constexpr auto kRANGE_MAX = 30;
constexpr auto kSEQ = 30;
constexpr auto kLEDGER_HASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
} // namespace
struct RPCFeatureHandlerTest : HandlerBaseTest {
RPCFeatureHandlerTest()
{
backend_->setRange(kRANGE_MIN, kRANGE_MAX);
}
protected:
StrictMockAmendmentCenterSharedPtr mockAmendmentCenterPtr_;
};
struct RPCFeatureHandlerParamTestCaseBundle {
std::string testName;
std::string testJson;
std::string expectedError;
std::string expectedErrorMessage;
};
// parameterized test cases for parameters check
struct RPCFeatureHandlerParamTest
: RPCFeatureHandlerTest,
testing::WithParamInterface<RPCFeatureHandlerParamTestCaseBundle> {};
static auto
generateTestValuesForParametersTest()
{
return std::vector<RPCFeatureHandlerParamTestCaseBundle>{
// Note: on rippled this and below returns "badFeature"
RPCFeatureHandlerParamTestCaseBundle{
.testName = "InvalidTypeFeatureBool",
.testJson = R"JSON({"feature": true})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
RPCFeatureHandlerParamTestCaseBundle{
.testName = "InvalidTypeFeatureInt",
.testJson = R"JSON({"feature": 42})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
RPCFeatureHandlerParamTestCaseBundle{
.testName = "InvalidTypeFeatureDouble",
.testJson = R"JSON({"feature": 4.2})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
RPCFeatureHandlerParamTestCaseBundle{
.testName = "InvalidTypeFeatureNull",
.testJson = R"JSON({"feature": null})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
// Note: this and below internal errors on rippled
RPCFeatureHandlerParamTestCaseBundle{
.testName = "InvalidTypeFeatureObj",
.testJson = R"JSON({"feature": {}})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
RPCFeatureHandlerParamTestCaseBundle{
.testName = "InvalidTypeFeatureArray",
.testJson = R"JSON({"feature": []})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
// "vetoed" should always be blocked
RPCFeatureHandlerParamTestCaseBundle{
.testName = "VetoedPassed",
.testJson = R"JSON({"feature": "foo", "vetoed": true})JSON",
.expectedError = "noPermission",
.expectedErrorMessage =
"The admin portion of feature API is not available through Clio."
},
RPCFeatureHandlerParamTestCaseBundle{
.testName = "InvalidTypeVetoedString",
.testJson = R"JSON({"feature": "foo", "vetoed": "test"})JSON",
.expectedError = "noPermission",
.expectedErrorMessage =
"The admin portion of feature API is not available through Clio."
},
RPCFeatureHandlerParamTestCaseBundle{
.testName = "InvalidTypeVetoedInt",
.testJson = R"JSON({"feature": "foo", "vetoed": 42})JSON",
.expectedError = "noPermission",
.expectedErrorMessage =
"The admin portion of feature API is not available through Clio."
},
RPCFeatureHandlerParamTestCaseBundle{
.testName = "InvalidTypeVetoedDouble",
.testJson = R"JSON({"feature": "foo", "vetoed": 4.2})JSON",
.expectedError = "noPermission",
.expectedErrorMessage =
"The admin portion of feature API is not available through Clio."
},
RPCFeatureHandlerParamTestCaseBundle{
.testName = "InvalidTypeVetoedObject",
.testJson = R"JSON({"feature": "foo", "vetoed": {}})JSON",
.expectedError = "noPermission",
.expectedErrorMessage =
"The admin portion of feature API is not available through Clio."
},
RPCFeatureHandlerParamTestCaseBundle{
.testName = "InvalidTypeVetoedArray",
.testJson = R"JSON({"feature": "foo", "vetoed": []})JSON",
.expectedError = "noPermission",
.expectedErrorMessage =
"The admin portion of feature API is not available through Clio."
},
};
}
INSTANTIATE_TEST_CASE_P(
RPCFeatureGroup1,
RPCFeatureHandlerParamTest,
testing::ValuesIn(generateTestValuesForParametersTest()),
tests::util::kNAME_GENERATOR
);
TEST_P(RPCFeatureHandlerParamTest, InvalidParams)
{
auto const testBundle = GetParam();
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{FeatureHandler{backend_, mockAmendmentCenterPtr_}};
auto const req = boost::json::parse(testBundle.testJson);
auto const output = handler.process(req, Context{.yield = yield, .apiVersion = 2});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), testBundle.expectedError);
EXPECT_EQ(err.at("error_message").as_string(), testBundle.expectedErrorMessage);
});
}
TEST_F(RPCFeatureHandlerTest, LedgerNotExistViaIntSequence)
{
EXPECT_CALL(*backend_, fetchLedgerBySequence(kRANGE_MAX, testing::_))
.WillOnce(testing::Return(std::nullopt));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{FeatureHandler{backend_, mockAmendmentCenterPtr_}};
auto const req = boost::json::parse(
fmt::format(
R"JSON({{
"ledger_index": {}
}})JSON",
kRANGE_MAX
)
);
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(), "lgrNotFound");
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
});
}
TEST_F(RPCFeatureHandlerTest, LedgerNotExistViaStringSequence)
{
EXPECT_CALL(*backend_, fetchLedgerBySequence(kRANGE_MAX, testing::_))
.WillOnce(testing::Return(std::nullopt));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{FeatureHandler{backend_, mockAmendmentCenterPtr_}};
auto const req = boost::json::parse(
fmt::format(
R"JSON({{
"ledger_index": "{}"
}})JSON",
kRANGE_MAX
)
);
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(), "lgrNotFound");
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
});
}
TEST_F(RPCFeatureHandlerTest, LedgerNotExistViaHash)
{
EXPECT_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLEDGER_HASH}, testing::_))
.WillOnce(testing::Return(std::nullopt));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{FeatureHandler{backend_, mockAmendmentCenterPtr_}};
auto const req = boost::json::parse(
fmt::format(
R"JSON({{
"ledger_hash": "{}"
}})JSON",
kLEDGER_HASH
)
);
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(), "lgrNotFound");
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
});
}
TEST_F(RPCFeatureHandlerTest, AlwaysNoPermissionForVetoed)
{
runSpawn([this](auto yield) {
auto const handler = AnyHandler{FeatureHandler{backend_, mockAmendmentCenterPtr_}};
auto const output = handler.process(
boost::json::parse(R"JSON({"vetoed": true, "feature": "foo"})JSON"), Context{yield}
);
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "noPermission");
EXPECT_EQ(
err.at("error_message").as_string(),
"The admin portion of feature API is not available through Clio."
);
});
}
TEST_F(RPCFeatureHandlerTest, SuccessPathViaNameWithSingleSupportedAndEnabledResult)
{
auto const all = std::vector<data::Amendment>{
{
.name = Amendments::fixUniversalNumber,
.feature = data::Amendment::getAmendmentId(Amendments::fixUniversalNumber),
.isSupportedByXRPL = true,
.isSupportedByClio = true,
},
{
.name = Amendments::fixRemoveNFTokenAutoTrustLine,
.feature = data::Amendment::getAmendmentId(Amendments::fixRemoveNFTokenAutoTrustLine),
.isSupportedByXRPL = true,
.isSupportedByClio = true,
}
};
auto const keys = std::vector<data::AmendmentKey>{Amendments::fixUniversalNumber};
auto const enabled = std::vector<bool>{true};
EXPECT_CALL(*mockAmendmentCenterPtr_, getAll).WillOnce(testing::ReturnRef(all));
EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(testing::_, keys, kSEQ))
.WillOnce(testing::Return(enabled));
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(testing::Return(ledgerHeader));
auto const expectedOutput = fmt::format(
R"JSON({{
"2E2FB9CF8A44EB80F4694D38AADAE9B8B7ADAFD2F092E10068E61C98C4F092B0": {{
"name": "fixUniversalNumber",
"enabled": true,
"supported": true
}},
"ledger_hash": "{}",
"ledger_index": {},
"validated": true
}})JSON",
kLEDGER_HASH,
kSEQ
);
runSpawn([this, &expectedOutput](auto yield) {
auto const handler = AnyHandler{FeatureHandler{backend_, mockAmendmentCenterPtr_}};
auto const output = handler.process(
boost::json::parse(R"JSON({"feature": "fixUniversalNumber"})JSON"), Context{yield}
);
ASSERT_TRUE(output);
EXPECT_EQ(*output.result, boost::json::parse(expectedOutput));
});
}
TEST_F(RPCFeatureHandlerTest, SuccessPathViaHashWithSingleResult)
{
auto const all = std::vector<data::Amendment>{
{
.name = Amendments::fixUniversalNumber,
.feature = data::Amendment::getAmendmentId(Amendments::fixUniversalNumber),
.isSupportedByXRPL = true,
.isSupportedByClio = true,
},
{
.name = Amendments::fixRemoveNFTokenAutoTrustLine,
.feature = data::Amendment::getAmendmentId(Amendments::fixRemoveNFTokenAutoTrustLine),
.isSupportedByXRPL = true,
.isSupportedByClio = true,
}
};
auto const keys = std::vector<data::AmendmentKey>{Amendments::fixUniversalNumber};
auto const enabled = std::vector<bool>{true};
EXPECT_CALL(*mockAmendmentCenterPtr_, getAll).WillOnce(testing::ReturnRef(all));
EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(testing::_, keys, kSEQ))
.WillOnce(testing::Return(enabled));
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(testing::Return(ledgerHeader));
auto const expectedOutput = fmt::format(
R"JSON({{
"2E2FB9CF8A44EB80F4694D38AADAE9B8B7ADAFD2F092E10068E61C98C4F092B0": {{
"name": "fixUniversalNumber",
"enabled": true,
"supported": true
}},
"ledger_hash": "{}",
"ledger_index": {},
"validated": true
}})JSON",
kLEDGER_HASH,
kSEQ
);
runSpawn([this, &expectedOutput](auto yield) {
auto const handler = AnyHandler{FeatureHandler{backend_, mockAmendmentCenterPtr_}};
auto const output = handler.process(
boost::json::parse(
R"JSON({"feature": "2E2FB9CF8A44EB80F4694D38AADAE9B8B7ADAFD2F092E10068E61C98C4F092B0"})JSON"
),
Context{yield}
);
ASSERT_TRUE(output);
EXPECT_EQ(*output.result, boost::json::parse(expectedOutput));
});
}
TEST_F(RPCFeatureHandlerTest, BadFeaturePath)
{
auto const all = std::vector<data::Amendment>{{
.name = Amendments::fixUniversalNumber,
.feature = data::Amendment::getAmendmentId(Amendments::fixUniversalNumber),
.isSupportedByXRPL = true,
.isSupportedByClio = true,
}};
auto const keys = std::vector<data::AmendmentKey>{"nonexistent"};
EXPECT_CALL(*mockAmendmentCenterPtr_, getAll).WillOnce(testing::ReturnRef(all));
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(testing::Return(ledgerHeader));
runSpawn([this](auto yield) {
auto const handler = AnyHandler{FeatureHandler{backend_, mockAmendmentCenterPtr_}};
auto const output = handler.process(
boost::json::parse(R"JSON({"feature": "nonexistent"})JSON"), Context{yield}
);
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "badFeature");
EXPECT_EQ(err.at("error_message").as_string(), "Feature unknown or invalid.");
});
}
TEST_F(RPCFeatureHandlerTest, DeletedLibXRPLAmendmentQueryByNameReturnsSupportedFalse)
{
auto const ownerPaysFeeKey =
ripple::to_string(data::Amendment::getAmendmentId(Amendments::OwnerPaysFee));
auto const all = std::vector<data::Amendment>{{
.name = Amendments::OwnerPaysFee,
.feature = data::Amendment::getAmendmentId(Amendments::OwnerPaysFee),
.isSupportedByXRPL = false,
.isSupportedByClio = true,
.isRetired = true,
}};
auto const keys = std::vector<data::AmendmentKey>{Amendments::OwnerPaysFee};
auto const enabled = std::vector<bool>{false};
EXPECT_CALL(*mockAmendmentCenterPtr_, getAll).WillOnce(testing::ReturnRef(all));
EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(testing::_, keys, kSEQ))
.WillOnce(testing::Return(enabled));
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(testing::Return(ledgerHeader));
auto const expectedOutput = fmt::format(
R"JSON({{
"{}": {{
"name": "OwnerPaysFee",
"enabled": false,
"supported": false
}},
"ledger_hash": "{}",
"ledger_index": {},
"validated": true
}})JSON",
ownerPaysFeeKey,
kLEDGER_HASH,
kSEQ
);
runSpawn([this, &expectedOutput](auto yield) {
auto const handler = AnyHandler{FeatureHandler{backend_, mockAmendmentCenterPtr_}};
auto const output = handler.process(
boost::json::parse(R"JSON({"feature": "OwnerPaysFee"})JSON"), Context{yield}
);
ASSERT_TRUE(output);
EXPECT_EQ(*output.result, boost::json::parse(expectedOutput));
});
}
TEST_F(RPCFeatureHandlerTest, DeletedLibXRPLAmendmentQueryByHashReturnsSupportedFalse)
{
auto const ownerPaysFeeKey =
ripple::to_string(data::Amendment::getAmendmentId(Amendments::OwnerPaysFee));
auto const all = std::vector<data::Amendment>{{
.name = Amendments::OwnerPaysFee,
.feature = data::Amendment::getAmendmentId(Amendments::OwnerPaysFee),
.isSupportedByXRPL = false,
.isSupportedByClio = true,
.isRetired = true,
}};
auto const keys = std::vector<data::AmendmentKey>{Amendments::OwnerPaysFee};
auto const enabled = std::vector<bool>{true};
EXPECT_CALL(*mockAmendmentCenterPtr_, getAll).WillOnce(testing::ReturnRef(all));
EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(testing::_, keys, kSEQ))
.WillOnce(testing::Return(enabled));
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, kSEQ);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(testing::Return(ledgerHeader));
auto const expectedOutput = fmt::format(
R"JSON({{
"{}": {{
"name": "OwnerPaysFee",
"enabled": true,
"supported": false
}},
"ledger_hash": "{}",
"ledger_index": {},
"validated": true
}})JSON",
ownerPaysFeeKey,
kLEDGER_HASH,
kSEQ
);
runSpawn([this, &ownerPaysFeeKey, &expectedOutput](auto yield) {
auto const handler = AnyHandler{FeatureHandler{backend_, mockAmendmentCenterPtr_}};
auto const req =
boost::json::parse(fmt::format(R"JSON({{"feature": "{}"}})JSON", ownerPaysFeeKey));
auto const output = handler.process(req, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(*output.result, boost::json::parse(expectedOutput));
});
}
TEST_F(RPCFeatureHandlerTest, SuccessPathWithMultipleResults)
{
auto const all = std::vector<data::Amendment>{
{
.name = Amendments::fixUniversalNumber,
.feature = data::Amendment::getAmendmentId(Amendments::fixUniversalNumber),
.isSupportedByXRPL = true,
.isSupportedByClio = true,
},
{
.name = Amendments::fixRemoveNFTokenAutoTrustLine,
.feature = data::Amendment::getAmendmentId(Amendments::fixRemoveNFTokenAutoTrustLine),
.isSupportedByXRPL = true,
.isSupportedByClio = false,
}
};
auto const keys = std::vector<data::AmendmentKey>{
Amendments::fixUniversalNumber, Amendments::fixRemoveNFTokenAutoTrustLine
};
auto const enabled = std::vector<bool>{true, false};
EXPECT_CALL(*mockAmendmentCenterPtr_, getAll).WillOnce(testing::ReturnRef(all));
EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(testing::_, keys, kSEQ))
.WillOnce(testing::Return(enabled));
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(testing::Return(ledgerHeader));
auto const amendments = createAmendmentsObject(
{Amendments::fixUniversalNumber, Amendments::fixRemoveNFTokenAutoTrustLine}
);
auto const expectedOutput = fmt::format(
R"JSON({{
"features": {{
"2E2FB9CF8A44EB80F4694D38AADAE9B8B7ADAFD2F092E10068E61C98C4F092B0": {{
"name": "fixUniversalNumber",
"enabled": true,
"supported": true
}},
"DF8B4536989BDACE3F934F29423848B9F1D76D09BE6A1FCFE7E7F06AA26ABEAD": {{
"name": "fixRemoveNFTokenAutoTrustLine",
"enabled": false,
"supported": false
}}
}},
"ledger_hash": "{}",
"ledger_index": {},
"validated": true
}})JSON",
kLEDGER_HASH,
kSEQ
);
runSpawn([this, &expectedOutput](auto yield) {
auto const handler = AnyHandler{FeatureHandler{backend_, mockAmendmentCenterPtr_}};
auto const output = handler.process(boost::json::parse(R"JSON({})JSON"), Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(*output.result, boost::json::parse(expectedOutput));
});
}