#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 #include #include #include #include #include #include #include using namespace rpc; using namespace data; namespace { constexpr auto kRangeMin = 10; constexpr auto kRangeMax = 30; constexpr auto kSeq = 30; constexpr auto kLedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; constexpr auto kApiVersion = 2; } // namespace struct RPCFeatureHandlerTest : HandlerBaseTest { RPCFeatureHandlerTest() { backend_->setRange(kRangeMin, kRangeMax); } 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 {}; static auto generateTestValuesForParametersTest() { return std::vector{ // 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::kNameGenerator ); 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 = kApiVersion}); 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(kRangeMax, 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", kRangeMax ) ); 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(kRangeMax, 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", kRangeMax ) ); 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{kLedgerHash}, 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", kLedgerHash ) ); 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{ { .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{Amendments::fixUniversalNumber}; auto const enabled = std::vector{true}; EXPECT_CALL(*mockAmendmentCenterPtr_, getAll).WillOnce(testing::ReturnRef(all)); EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(testing::_, keys, kSeq)) .WillOnce(testing::Return(enabled)); auto const ledgerHeader = createLedgerHeader(kLedgerHash, 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", kLedgerHash, 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{ { .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{Amendments::fixUniversalNumber}; auto const enabled = std::vector{true}; EXPECT_CALL(*mockAmendmentCenterPtr_, getAll).WillOnce(testing::ReturnRef(all)); EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(testing::_, keys, kSeq)) .WillOnce(testing::Return(enabled)); auto const ledgerHeader = createLedgerHeader(kLedgerHash, 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", kLedgerHash, 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{{ .name = Amendments::fixUniversalNumber, .feature = data::Amendment::getAmendmentId(Amendments::fixUniversalNumber), .isSupportedByXRPL = true, .isSupportedByClio = true, }}; auto const keys = std::vector{"nonexistent"}; EXPECT_CALL(*mockAmendmentCenterPtr_, getAll).WillOnce(testing::ReturnRef(all)); auto const ledgerHeader = createLedgerHeader(kLedgerHash, 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{{ .name = Amendments::OwnerPaysFee, .feature = data::Amendment::getAmendmentId(Amendments::OwnerPaysFee), .isSupportedByXRPL = false, .isSupportedByClio = true, .isRetired = true, }}; auto const keys = std::vector{Amendments::OwnerPaysFee}; auto const enabled = std::vector{false}; EXPECT_CALL(*mockAmendmentCenterPtr_, getAll).WillOnce(testing::ReturnRef(all)); EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(testing::_, keys, kSeq)) .WillOnce(testing::Return(enabled)); auto const ledgerHeader = createLedgerHeader(kLedgerHash, 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, kLedgerHash, 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{{ .name = Amendments::OwnerPaysFee, .feature = data::Amendment::getAmendmentId(Amendments::OwnerPaysFee), .isSupportedByXRPL = false, .isSupportedByClio = true, .isRetired = true, }}; auto const keys = std::vector{Amendments::OwnerPaysFee}; auto const enabled = std::vector{true}; EXPECT_CALL(*mockAmendmentCenterPtr_, getAll).WillOnce(testing::ReturnRef(all)); EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(testing::_, keys, kSeq)) .WillOnce(testing::Return(enabled)); auto const ledgerHeader = createLedgerHeader(kLedgerHash, 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, kLedgerHash, 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{ { .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{ Amendments::fixUniversalNumber, Amendments::fixRemoveNFTokenAutoTrustLine }; auto const enabled = std::vector{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(kLedgerHash, 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", kLedgerHash, 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)); }); }