#include "data/Types.hpp" #include "rpc/Errors.hpp" #include "rpc/common/impl/ForwardingProxy.hpp" #include "util/HandlerBaseTestFixture.hpp" #include "util/MockCounters.hpp" #include "util/MockHandlerProvider.hpp" #include "util/MockLoadBalancer.hpp" #include "util/NameGenerator.hpp" #include "util/Taggable.hpp" #include "util/config/ConfigDefinition.hpp" #include "util/config/ConfigValue.hpp" #include "util/config/Types.hpp" #include "web/Context.hpp" #include #include #include #include #include #include #include #include #include using namespace rpc; using namespace testing; using namespace util::config; namespace json = boost::json; namespace { constexpr auto kCLIENT_IP = "127.0.0.1"; } // namespace class RPCForwardingProxyTest : public HandlerBaseTest { protected: std::shared_ptr loadBalancer_ = std::make_shared(); std::shared_ptr handlerProvider_ = std::make_shared(); MockCounters counters_; ClioConfigDefinition const config_{ {"log.tag_style", ConfigValue{ConfigType::String}.defaultValue("none")} }; util::TagDecoratorFactory tagFactory_{config_}; rpc::impl::ForwardingProxy proxy_{ loadBalancer_, counters_, handlerProvider_ }; }; struct ShouldForwardParamTestCaseBundle { std::string testName; std::uint32_t apiVersion; std::string method; std::string testJson; bool mockedIsClioOnly; std::uint32_t called; bool isAdmin; bool expected; }; struct ShouldForwardParameterTest : public RPCForwardingProxyTest, WithParamInterface {}; static auto generateTestValuesForParametersTest() { auto const isClioOnly = true; auto const isAdmin = true; auto const shouldForward = true; return std::vector{ {.testName = "ShouldForwardReturnsFalseIfClioOnly", .apiVersion = 2u, .method = "test", .testJson = "{}", .mockedIsClioOnly = isClioOnly, .called = 1, .isAdmin = !isAdmin, .expected = !shouldForward}, {.testName = "ShouldForwardReturnsTrueIfProxied", .apiVersion = 2u, .method = "submit", .testJson = "{}", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = !isAdmin, .expected = shouldForward}, {.testName = "ShouldForwardReturnsTrueIsNotAdminSimulate", .apiVersion = 1u, .method = "simulate", .testJson = "{}", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = !isAdmin, .expected = shouldForward}, {.testName = "ShouldForwardReturnsTrueIsAdminSimulate", .apiVersion = 1u, .method = "simulate", .testJson = "{}", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = isAdmin, .expected = shouldForward}, {.testName = "ShouldForwardReturnsTrueIsNotAdminRipplePathFind", .apiVersion = 2u, .method = "ripple_path_find", .testJson = R"JSON({"force_forward": true})JSON", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = !isAdmin, .expected = shouldForward}, {.testName = "ShouldForwardReturnsTrueIsAdminRipplePathFind", .apiVersion = 2u, .method = "ripple_path_find", .testJson = R"JSON({"force_forward": true})JSON", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = isAdmin, .expected = shouldForward}, {.testName = "ShouldForwardReturnsTrueIfCurrentLedgerSpecified", .apiVersion = 2u, .method = "anymethod", .testJson = R"JSON({"ledger_index": "current"})JSON", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = !isAdmin, .expected = shouldForward}, {.testName = "ShouldForwardReturnsTrueIfClosedLedgerSpecified", .apiVersion = 2u, .method = "anymethod", .testJson = R"JSON({"ledger_index": "closed"})JSON", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = !isAdmin, .expected = shouldForward}, {.testName = "ShouldForwardReturnsTrueIfAccountInfoWithQueueSpecified", .apiVersion = 2u, .method = "account_info", .testJson = R"JSON({"queue": true})JSON", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = !isAdmin, .expected = shouldForward}, {.testName = "ShouldForwardReturnsFalseIfAccountInfoQueueIsFalse", .apiVersion = 2u, .method = "account_info", .testJson = R"JSON({"queue": false})JSON", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = !isAdmin, .expected = !shouldForward}, {.testName = "ShouldForwardReturnsTrueIfLedgerWithQueueSpecified", .apiVersion = 2u, .method = "ledger", .testJson = R"JSON({"queue": true})JSON", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = !isAdmin, .expected = shouldForward}, {.testName = "ShouldForwardReturnsFalseIfLedgerQueueIsFalse", .apiVersion = 2u, .method = "ledger", .testJson = R"JSON({"queue": false})JSON", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = !isAdmin, .expected = !shouldForward}, {.testName = "ShouldNotForwardReturnsTrueIfAPIVersionIsV1", .apiVersion = 1u, .method = "api_version_check", .testJson = "{}", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = !isAdmin, .expected = !shouldForward}, {.testName = "ShouldForwardReturnsFalseIfAPIVersionIsV2", .apiVersion = 2u, .method = "api_version_check", .testJson = "{}", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = !isAdmin, .expected = !shouldForward}, {.testName = "ShouldNeverForwardSubscribe", .apiVersion = 1u, .method = "subscribe", .testJson = "{}", .mockedIsClioOnly = !isClioOnly, .called = 0, .isAdmin = !isAdmin, .expected = !shouldForward}, {.testName = "ShouldNeverForwardUnsubscribe", .apiVersion = 1u, .method = "unsubscribe", .testJson = "{}", .mockedIsClioOnly = !isClioOnly, .called = 0, .isAdmin = !isAdmin, .expected = !shouldForward}, {.testName = "ForceForwardTrue", .apiVersion = 1u, .method = "any_method", .testJson = R"JSON({"force_forward": true})JSON", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = isAdmin, .expected = shouldForward}, {.testName = "ForceForwardFalse", .apiVersion = 1u, .method = "any_method", .testJson = R"JSON({"force_forward": false})JSON", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = isAdmin, .expected = !shouldForward}, {.testName = "ForceForwardNotAdmin", .apiVersion = 1u, .method = "any_method", .testJson = R"JSON({"force_forward": true})JSON", .mockedIsClioOnly = !isClioOnly, .called = 1, .isAdmin = !isAdmin, .expected = !shouldForward}, {.testName = "ForceForwardSubscribe", .apiVersion = 1u, .method = "subscribe", .testJson = R"JSON({"force_forward": true})JSON", .mockedIsClioOnly = !isClioOnly, .called = 0, .isAdmin = isAdmin, .expected = not shouldForward}, {.testName = "ForceForwardUnsubscribe", .apiVersion = 1u, .method = "unsubscribe", .testJson = R"JSON({"force_forward": true})JSON", .mockedIsClioOnly = !isClioOnly, .called = 0, .isAdmin = isAdmin, .expected = !shouldForward}, {.testName = "ForceForwardClioOnly", .apiVersion = 1u, .method = "clio_only_method", .testJson = R"JSON({"force_forward": true})JSON", .mockedIsClioOnly = isClioOnly, .called = 1, .isAdmin = isAdmin, .expected = !shouldForward}, }; } INSTANTIATE_TEST_CASE_P( ShouldForwardTest, ShouldForwardParameterTest, ValuesIn(generateTestValuesForParametersTest()), tests::util::kNAME_GENERATOR ); TEST_P(ShouldForwardParameterTest, Test) { auto const testBundle = GetParam(); auto const rawHandlerProviderPtr = handlerProvider_.get(); auto const apiVersion = testBundle.apiVersion; auto const method = testBundle.method; auto const params = json::parse(testBundle.testJson); ON_CALL(*rawHandlerProviderPtr, isClioOnly(_)) .WillByDefault(Return(testBundle.mockedIsClioOnly)); EXPECT_CALL(*rawHandlerProviderPtr, isClioOnly(method)).Times(testBundle.called); runSpawn([&](auto yield) { auto const ctx = web::Context{ yield, method, apiVersion, params.as_object(), nullptr, tagFactory_, data::LedgerRange{}, kCLIENT_IP, testBundle.isAdmin, }; auto const res = proxy_.shouldForward(ctx); ASSERT_EQ(res, testBundle.expected); }); } TEST_F(RPCForwardingProxyTest, ForwardCallsBalancerWithCorrectParams) { auto const rawHandlerProviderPtr = handlerProvider_.get(); auto const rawBalancerPtr = loadBalancer_.get(); auto const apiVersion = 2u; auto const method = "submit"; auto const params = json::parse(R"JSON({"test": true})JSON"); auto const forwarded = json::parse(R"JSON({"test": true, "command": "submit"})JSON"); EXPECT_CALL( *rawBalancerPtr, forwardToRippled( forwarded.as_object(), std::make_optional(kCLIENT_IP), true, _ ) ) .WillOnce(Return(json::object{})); EXPECT_CALL(*rawHandlerProviderPtr, contains(method)).WillOnce(Return(true)); EXPECT_CALL(counters_, rpcForwarded(method)); runSpawn([&](auto yield) { auto const ctx = web::Context{ yield, method, apiVersion, params.as_object(), nullptr, tagFactory_, data::LedgerRange{}, kCLIENT_IP, true, }; auto const res = proxy_.forward(ctx); EXPECT_TRUE(res.response.has_value()); }); } TEST_F(RPCForwardingProxyTest, ForwardingFailYieldsErrorStatus) { auto const rawHandlerProviderPtr = handlerProvider_.get(); auto const rawBalancerPtr = loadBalancer_.get(); auto const apiVersion = 2u; auto const method = "submit"; auto const params = json::parse(R"JSON({"test": true})JSON"); auto const forwarded = json::parse(R"JSON({"test": true, "command": "submit"})JSON"); EXPECT_CALL( *rawBalancerPtr, forwardToRippled( forwarded.as_object(), std::make_optional(kCLIENT_IP), true, _ ) ) .WillOnce(Return(std::unexpected{rpc::ClioError::EtlInvalidResponse})); EXPECT_CALL(*rawHandlerProviderPtr, contains(method)).WillOnce(Return(true)); EXPECT_CALL(counters_, rpcFailedToForward(method)); runSpawn([&](auto yield) { auto const ctx = web::Context{ yield, method, apiVersion, params.as_object(), nullptr, tagFactory_, data::LedgerRange{}, kCLIENT_IP, true, }; auto const res = proxy_.forward(ctx); EXPECT_FALSE(res.response.has_value()); EXPECT_EQ(res.response.error(), rpc::ClioError::EtlInvalidResponse); }); }