mirror of
https://github.com/XRPLF/clio.git
synced 2025-12-06 17:27:58 +00:00
@@ -23,7 +23,6 @@ target_sources(
|
||||
etl/ETLStateTests.cpp
|
||||
etl/ExtractionDataPipeTests.cpp
|
||||
etl/ExtractorTests.cpp
|
||||
etl/ForwardingCacheTests.cpp
|
||||
etl/ForwardingSourceTests.cpp
|
||||
etl/GrpcSourceTests.cpp
|
||||
etl/LedgerPublisherTests.cpp
|
||||
@@ -92,6 +91,7 @@ target_sources(
|
||||
rpc/handlers/UnsubscribeTests.cpp
|
||||
rpc/handlers/VersionHandlerTests.cpp
|
||||
rpc/JsonBoolTests.cpp
|
||||
rpc/RPCEngineTests.cpp
|
||||
rpc/RPCHelpersTests.cpp
|
||||
rpc/WorkQueueTests.cpp
|
||||
util/AccountUtilsTests.cpp
|
||||
@@ -121,6 +121,7 @@ target_sources(
|
||||
util/RandomTests.cpp
|
||||
util/RetryTests.cpp
|
||||
util/RepeatTests.cpp
|
||||
util/ResponseExpirationCacheTests.cpp
|
||||
util/SignalsHandlerTests.cpp
|
||||
util/TimeUtilsTests.cpp
|
||||
util/TxUtilTests.cpp
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "etl/impl/ForwardingCache.hpp"
|
||||
|
||||
#include <boost/json/object.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
using namespace etl::impl;
|
||||
|
||||
struct CacheEntryTests : public ::testing::Test {
|
||||
CacheEntry entry_;
|
||||
boost::json::object const object_ = {{"key", "value"}};
|
||||
};
|
||||
|
||||
TEST_F(CacheEntryTests, PutAndGet)
|
||||
{
|
||||
EXPECT_FALSE(entry_.get());
|
||||
|
||||
entry_.put(object_);
|
||||
auto result = entry_.get();
|
||||
|
||||
ASSERT_TRUE(result);
|
||||
EXPECT_EQ(*result, object_);
|
||||
}
|
||||
|
||||
TEST_F(CacheEntryTests, LastUpdated)
|
||||
{
|
||||
EXPECT_EQ(entry_.lastUpdated().time_since_epoch().count(), 0);
|
||||
|
||||
entry_.put(object_);
|
||||
auto const lastUpdated = entry_.lastUpdated();
|
||||
|
||||
EXPECT_GE(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - lastUpdated).count(), 0
|
||||
);
|
||||
|
||||
entry_.put(boost::json::object{{"key", "new value"}});
|
||||
auto const newLastUpdated = entry_.lastUpdated();
|
||||
EXPECT_GT(newLastUpdated, lastUpdated);
|
||||
EXPECT_GE(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - newLastUpdated)
|
||||
.count(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(CacheEntryTests, Invalidate)
|
||||
{
|
||||
entry_.put(object_);
|
||||
entry_.invalidate();
|
||||
EXPECT_FALSE(entry_.get());
|
||||
}
|
||||
|
||||
TEST(ForwardingCacheTests, ShouldCache)
|
||||
{
|
||||
for (auto const& command : ForwardingCache::CACHEABLE_COMMANDS) {
|
||||
auto const request = boost::json::object{{"command", command}};
|
||||
EXPECT_TRUE(ForwardingCache::shouldCache(request));
|
||||
}
|
||||
auto const request = boost::json::object{{"command", "tx"}};
|
||||
EXPECT_FALSE(ForwardingCache::shouldCache(request));
|
||||
|
||||
auto const requestWithoutCommand = boost::json::object{{"key", "value"}};
|
||||
EXPECT_FALSE(ForwardingCache::shouldCache(requestWithoutCommand));
|
||||
}
|
||||
|
||||
TEST(ForwardingCacheTests, Get)
|
||||
{
|
||||
ForwardingCache cache{std::chrono::seconds{100}};
|
||||
auto const request = boost::json::object{{"command", "server_info"}};
|
||||
auto const response = boost::json::object{{"key", "value"}};
|
||||
|
||||
cache.put(request, response);
|
||||
auto const result = cache.get(request);
|
||||
|
||||
ASSERT_TRUE(result);
|
||||
EXPECT_EQ(*result, response);
|
||||
}
|
||||
|
||||
TEST(ForwardingCacheTests, GetExpired)
|
||||
{
|
||||
ForwardingCache cache{std::chrono::milliseconds{1}};
|
||||
auto const request = boost::json::object{{"command", "server_info"}};
|
||||
auto const response = boost::json::object{{"key", "value"}};
|
||||
|
||||
cache.put(request, response);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{2});
|
||||
|
||||
auto const result = cache.get(request);
|
||||
EXPECT_FALSE(result);
|
||||
}
|
||||
|
||||
TEST(ForwardingCacheTests, GetAndPutNotCommand)
|
||||
{
|
||||
ForwardingCache cache{std::chrono::seconds{100}};
|
||||
auto const request = boost::json::object{{"key", "value"}};
|
||||
auto const response = boost::json::object{{"key", "value"}};
|
||||
cache.put(request, response);
|
||||
auto const result = cache.get(request);
|
||||
EXPECT_FALSE(result);
|
||||
}
|
||||
|
||||
TEST(ForwardingCache, Invalidate)
|
||||
{
|
||||
ForwardingCache cache{std::chrono::seconds{100}};
|
||||
auto const request = boost::json::object{{"command", "server_info"}};
|
||||
auto const response = boost::json::object{{"key", "value"}};
|
||||
|
||||
cache.put(request, response);
|
||||
cache.invalidate();
|
||||
|
||||
EXPECT_FALSE(cache.get(request));
|
||||
}
|
||||
@@ -519,7 +519,7 @@ struct LoadBalancerForwardToRippledTests : LoadBalancerConstructorTests, SyncAsi
|
||||
EXPECT_CALL(sourceFactory_.sourceAt(1), run);
|
||||
}
|
||||
|
||||
boost::json::object const request_{{"request", "value"}};
|
||||
boost::json::object const request_{{"command", "value"}};
|
||||
std::optional<std::string> const clientIP_ = "some_ip";
|
||||
boost::json::object const response_{{"response", "other_value"}};
|
||||
};
|
||||
@@ -699,6 +699,21 @@ TEST_F(LoadBalancerForwardToRippledTests, onLedgerClosedHookInvalidatesCache)
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(LoadBalancerForwardToRippledTests, commandLineMissing)
|
||||
{
|
||||
EXPECT_CALL(sourceFactory_, makeSource).Times(2);
|
||||
auto loadBalancer = makeLoadBalancer();
|
||||
|
||||
auto const request = boost::json::object{{"command2", "server_info"}};
|
||||
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
EXPECT_EQ(
|
||||
loadBalancer->forwardToRippled(request, clientIP_, false, yield).error(),
|
||||
rpc::ClioError::rpcCOMMAND_IS_MISSING
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
struct LoadBalancerToJsonTests : LoadBalancerOnConnectHookTests {};
|
||||
|
||||
TEST_F(LoadBalancerToJsonTests, toJson)
|
||||
|
||||
477
tests/unit/rpc/RPCEngineTests.cpp
Normal file
477
tests/unit/rpc/RPCEngineTests.cpp
Normal file
@@ -0,0 +1,477 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "data/BackendInterface.hpp"
|
||||
#include "data/Types.hpp"
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/FakesAndMocks.hpp"
|
||||
#include "rpc/RPCEngine.hpp"
|
||||
#include "rpc/WorkQueue.hpp"
|
||||
#include "rpc/common/AnyHandler.hpp"
|
||||
#include "util/AsioContextTestFixture.hpp"
|
||||
#include "util/MockBackendTestFixture.hpp"
|
||||
#include "util/MockCounters.hpp"
|
||||
#include "util/MockCountersFixture.hpp"
|
||||
#include "util/MockETLServiceTestFixture.hpp"
|
||||
#include "util/MockHandlerProvider.hpp"
|
||||
#include "util/MockLoadBalancer.hpp"
|
||||
#include "util/MockPrometheus.hpp"
|
||||
#include "util/NameGenerator.hpp"
|
||||
#include "util/Taggable.hpp"
|
||||
#include "util/config/Config.hpp"
|
||||
#include "web/Context.hpp"
|
||||
#include "web/dosguard/DOSGuard.hpp"
|
||||
#include "web/dosguard/WhitelistHandler.hpp"
|
||||
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
using namespace rpc;
|
||||
using namespace util;
|
||||
namespace json = boost::json;
|
||||
using namespace testing;
|
||||
|
||||
namespace {
|
||||
constexpr auto FORWARD_REPLY = R"JSON({
|
||||
"result":
|
||||
{
|
||||
"status": "success",
|
||||
"forwarded": true
|
||||
}
|
||||
})JSON";
|
||||
} // namespace
|
||||
|
||||
struct RPCEngineTest : util::prometheus::WithPrometheus,
|
||||
MockBackendTest,
|
||||
MockCountersTest,
|
||||
MockLoadBalancerTest,
|
||||
SyncAsioContextTest {
|
||||
Config cfg = Config{json::parse(R"JSON({
|
||||
"server": {"max_queue_size": 2},
|
||||
"workers": 4
|
||||
})JSON")};
|
||||
util::TagDecoratorFactory tagFactory{cfg};
|
||||
WorkQueue queue = WorkQueue::make_WorkQueue(cfg);
|
||||
web::dosguard::WhitelistHandler whitelistHandler{cfg};
|
||||
web::dosguard::DOSGuard dosGuard{cfg, whitelistHandler};
|
||||
std::shared_ptr<MockHandlerProvider> handlerProvider = std::make_shared<MockHandlerProvider>();
|
||||
};
|
||||
|
||||
struct RPCEngineFlowTestCaseBundle {
|
||||
std::string testName;
|
||||
bool isAdmin;
|
||||
std::string method;
|
||||
std::string params;
|
||||
bool forwarded;
|
||||
std::optional<bool> isTooBusy;
|
||||
std::optional<bool> isUnknownCmd;
|
||||
bool handlerReturnError;
|
||||
std::optional<rpc::Status> status;
|
||||
std::optional<boost::json::object> response;
|
||||
};
|
||||
|
||||
struct RPCEngineFlowParameterTest : public RPCEngineTest, WithParamInterface<RPCEngineFlowTestCaseBundle> {};
|
||||
|
||||
static auto
|
||||
generateTestValuesForParametersTest()
|
||||
{
|
||||
auto const neverCalled = std::nullopt;
|
||||
|
||||
return std::vector<RPCEngineFlowTestCaseBundle>{
|
||||
{.testName = "ForwardedSubmit",
|
||||
.isAdmin = true,
|
||||
.method = "submit",
|
||||
.params = "{}",
|
||||
.forwarded = true,
|
||||
.isTooBusy = neverCalled,
|
||||
.isUnknownCmd = neverCalled,
|
||||
.handlerReturnError = false,
|
||||
.status = rpc::Status{},
|
||||
.response = boost::json::parse(FORWARD_REPLY).as_object()},
|
||||
{.testName = "ForwardAdminCmd",
|
||||
.isAdmin = false,
|
||||
.method = "ledger",
|
||||
.params = R"JSON({"full": true, "ledger_index": "current"})JSON",
|
||||
.forwarded = false,
|
||||
.isTooBusy = neverCalled,
|
||||
.isUnknownCmd = neverCalled,
|
||||
.handlerReturnError = false,
|
||||
.status = rpc::Status{RippledError::rpcNO_PERMISSION},
|
||||
.response = std::nullopt},
|
||||
{.testName = "BackendTooBusy",
|
||||
.isAdmin = false,
|
||||
.method = "ledger",
|
||||
.params = "{}",
|
||||
.forwarded = false,
|
||||
.isTooBusy = true,
|
||||
.isUnknownCmd = neverCalled,
|
||||
.handlerReturnError = false,
|
||||
.status = rpc::Status{RippledError::rpcTOO_BUSY},
|
||||
.response = std::nullopt},
|
||||
{.testName = "HandlerUnknown",
|
||||
.isAdmin = false,
|
||||
.method = "ledger",
|
||||
.params = "{}",
|
||||
.forwarded = false,
|
||||
.isTooBusy = false,
|
||||
.isUnknownCmd = true,
|
||||
.handlerReturnError = false,
|
||||
.status = rpc::Status{RippledError::rpcUNKNOWN_COMMAND},
|
||||
.response = std::nullopt},
|
||||
{.testName = "HandlerReturnError",
|
||||
.isAdmin = false,
|
||||
.method = "ledger",
|
||||
.params = R"JSON({"hello": "world", "limit": 50})JSON",
|
||||
.forwarded = false,
|
||||
.isTooBusy = false,
|
||||
.isUnknownCmd = false,
|
||||
.handlerReturnError = true,
|
||||
.status = rpc::Status{"Very custom error"},
|
||||
.response = std::nullopt},
|
||||
{.testName = "HandlerReturnResponse",
|
||||
.isAdmin = false,
|
||||
.method = "ledger",
|
||||
.params = R"JSON({"hello": "world", "limit": 50})JSON",
|
||||
.forwarded = false,
|
||||
.isTooBusy = false,
|
||||
.isUnknownCmd = false,
|
||||
.handlerReturnError = false,
|
||||
.status = std::nullopt,
|
||||
.response = boost::json::parse(R"JSON({"computed": "world_50"})JSON").as_object()},
|
||||
};
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
RPCEngineFlow,
|
||||
RPCEngineFlowParameterTest,
|
||||
ValuesIn(generateTestValuesForParametersTest()),
|
||||
tests::util::NameGenerator
|
||||
);
|
||||
|
||||
TEST_P(RPCEngineFlowParameterTest, Test)
|
||||
{
|
||||
auto const& testBundle = GetParam();
|
||||
|
||||
std::shared_ptr<RPCEngine<MockLoadBalancer, MockCounters>> engine =
|
||||
RPCEngine<MockLoadBalancer, MockCounters>::make_RPCEngine(
|
||||
Config{}, backend, mockLoadBalancerPtr, dosGuard, queue, *mockCountersPtr, handlerProvider
|
||||
);
|
||||
|
||||
if (testBundle.forwarded) {
|
||||
EXPECT_CALL(*mockLoadBalancerPtr, forwardToRippled)
|
||||
.WillOnce(Return(std::expected<boost::json::object, rpc::ClioError>(json::parse(FORWARD_REPLY).as_object()))
|
||||
);
|
||||
EXPECT_CALL(*handlerProvider, contains).WillOnce(Return(true));
|
||||
EXPECT_CALL(*mockCountersPtr, rpcForwarded(testBundle.method));
|
||||
}
|
||||
|
||||
if (testBundle.isTooBusy.has_value()) {
|
||||
EXPECT_CALL(*backend, isTooBusy).WillOnce(Return(*testBundle.isTooBusy));
|
||||
if (testBundle.isTooBusy.value())
|
||||
EXPECT_CALL(*mockCountersPtr, onTooBusy);
|
||||
}
|
||||
|
||||
EXPECT_CALL(*handlerProvider, isClioOnly).WillOnce(Return(false));
|
||||
|
||||
if (testBundle.isUnknownCmd.has_value()) {
|
||||
if (testBundle.isUnknownCmd.value()) {
|
||||
EXPECT_CALL(*handlerProvider, getHandler).WillOnce(Return(std::nullopt));
|
||||
EXPECT_CALL(*mockCountersPtr, onUnknownCommand);
|
||||
} else {
|
||||
if (testBundle.handlerReturnError) {
|
||||
EXPECT_CALL(*handlerProvider, getHandler)
|
||||
.WillOnce(Return(AnyHandler{tests::common::FailingHandlerFake{}}));
|
||||
EXPECT_CALL(*mockCountersPtr, rpcErrored(testBundle.method));
|
||||
EXPECT_CALL(*handlerProvider, contains(testBundle.method)).WillOnce(Return(true));
|
||||
} else {
|
||||
EXPECT_CALL(*handlerProvider, getHandler(testBundle.method))
|
||||
.WillOnce(Return(AnyHandler{tests::common::HandlerFake{}}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runSpawn([&](auto yield) {
|
||||
auto const ctx = web::Context(
|
||||
yield,
|
||||
testBundle.method,
|
||||
1, // api version
|
||||
boost::json::parse(testBundle.params).as_object(),
|
||||
nullptr,
|
||||
tagFactory,
|
||||
LedgerRange{0, 30},
|
||||
"127.0.0.2",
|
||||
testBundle.isAdmin
|
||||
);
|
||||
|
||||
auto const res = engine->buildResponse(ctx);
|
||||
auto const status = std::get_if<rpc::Status>(&res.response);
|
||||
auto const response = std::get_if<boost::json::object>(&res.response);
|
||||
ASSERT_EQ(status == nullptr, testBundle.response.has_value());
|
||||
if (testBundle.response.has_value()) {
|
||||
EXPECT_EQ(*response, testBundle.response.value());
|
||||
} else {
|
||||
EXPECT_TRUE(*status == testBundle.status.value());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCEngineTest, ThrowDatabaseError)
|
||||
{
|
||||
auto const method = "subscribe";
|
||||
std::shared_ptr<RPCEngine<MockLoadBalancer, MockCounters>> engine =
|
||||
RPCEngine<MockLoadBalancer, MockCounters>::make_RPCEngine(
|
||||
cfg, backend, mockLoadBalancerPtr, dosGuard, queue, *mockCountersPtr, handlerProvider
|
||||
);
|
||||
EXPECT_CALL(*backend, isTooBusy).WillOnce(Return(false));
|
||||
EXPECT_CALL(*handlerProvider, getHandler(method)).WillOnce(Return(AnyHandler{tests::common::FailingHandlerFake{}}));
|
||||
EXPECT_CALL(*mockCountersPtr, rpcErrored(method)).WillOnce(Throw(data::DatabaseTimeout{}));
|
||||
EXPECT_CALL(*handlerProvider, contains(method)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*mockCountersPtr, onTooBusy());
|
||||
|
||||
runSpawn([&](auto yield) {
|
||||
auto const ctx = web::Context(
|
||||
yield,
|
||||
method,
|
||||
1,
|
||||
boost::json::parse("{}").as_object(),
|
||||
nullptr,
|
||||
tagFactory,
|
||||
LedgerRange{0, 30},
|
||||
"127.0.0.2",
|
||||
false
|
||||
);
|
||||
|
||||
auto const res = engine->buildResponse(ctx);
|
||||
auto const status = std::get_if<rpc::Status>(&res.response);
|
||||
ASSERT_TRUE(status != nullptr);
|
||||
EXPECT_TRUE(*status == Status{RippledError::rpcTOO_BUSY});
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCEngineTest, ThrowException)
|
||||
{
|
||||
auto const method = "subscribe";
|
||||
std::shared_ptr<RPCEngine<MockLoadBalancer, MockCounters>> engine =
|
||||
RPCEngine<MockLoadBalancer, MockCounters>::make_RPCEngine(
|
||||
cfg, backend, mockLoadBalancerPtr, dosGuard, queue, *mockCountersPtr, handlerProvider
|
||||
);
|
||||
EXPECT_CALL(*backend, isTooBusy).WillOnce(Return(false));
|
||||
EXPECT_CALL(*handlerProvider, getHandler(method)).WillOnce(Return(AnyHandler{tests::common::FailingHandlerFake{}}));
|
||||
EXPECT_CALL(*mockCountersPtr, rpcErrored(method)).WillOnce(Throw(std::exception{}));
|
||||
EXPECT_CALL(*handlerProvider, contains(method)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*mockCountersPtr, onInternalError());
|
||||
|
||||
runSpawn([&](auto yield) {
|
||||
auto const ctx = web::Context(
|
||||
yield,
|
||||
method,
|
||||
1,
|
||||
boost::json::parse("{}").as_object(),
|
||||
nullptr,
|
||||
tagFactory,
|
||||
LedgerRange{0, 30},
|
||||
"127.0.0.2",
|
||||
false
|
||||
);
|
||||
|
||||
auto const res = engine->buildResponse(ctx);
|
||||
auto const status = std::get_if<rpc::Status>(&res.response);
|
||||
ASSERT_TRUE(status != nullptr);
|
||||
EXPECT_TRUE(*status == Status{RippledError::rpcINTERNAL});
|
||||
});
|
||||
}
|
||||
|
||||
struct RPCEngineCacheTestCaseBundle {
|
||||
std::string testName;
|
||||
std::string config;
|
||||
std::string method;
|
||||
bool isAdmin;
|
||||
bool expectedCacheEnabled;
|
||||
};
|
||||
|
||||
struct RPCEngineCacheParameterTest : public RPCEngineTest, WithParamInterface<RPCEngineCacheTestCaseBundle> {};
|
||||
|
||||
static auto
|
||||
generateCacheTestValuesForParametersTest()
|
||||
{
|
||||
return std::vector<RPCEngineCacheTestCaseBundle>{
|
||||
{.testName = "CacheEnabled",
|
||||
.config = R"JSON({
|
||||
"server": {"max_queue_size": 2},
|
||||
"workers": 4,
|
||||
"rpc":
|
||||
{"cache_timeout": 10}
|
||||
})JSON",
|
||||
.method = "server_info",
|
||||
.isAdmin = false,
|
||||
.expectedCacheEnabled = true},
|
||||
{.testName = "CacheDisabledWhenNoConfig",
|
||||
.config = R"JSON({
|
||||
"server": {"max_queue_size": 2},
|
||||
"workers": 4,
|
||||
"rpc": {}
|
||||
})JSON",
|
||||
.method = "server_info",
|
||||
.isAdmin = false,
|
||||
.expectedCacheEnabled = false},
|
||||
{.testName = "CacheDisabledWhenNoTimeout",
|
||||
.config = R"JSON({
|
||||
"server": {"max_queue_size": 2},
|
||||
"workers": 4,
|
||||
"rpc": {}
|
||||
})JSON",
|
||||
.method = "server_info",
|
||||
.isAdmin = false,
|
||||
.expectedCacheEnabled = false},
|
||||
{.testName = "CacheDisabledWhenTimeoutIsZero",
|
||||
.config = R"JSON({
|
||||
"server": {"max_queue_size": 2},
|
||||
"workers": 4,
|
||||
"rpc": {"cache_timeout": 0}
|
||||
})JSON",
|
||||
.method = "server_info",
|
||||
.isAdmin = false,
|
||||
.expectedCacheEnabled = false},
|
||||
{.testName = "CacheNotWorkForAdmin",
|
||||
.config = R"JSON({
|
||||
"server": {"max_queue_size": 2},
|
||||
"workers": 4,
|
||||
"rpc": { "cache_timeout": 10}
|
||||
})JSON",
|
||||
.method = "server_info",
|
||||
.isAdmin = true,
|
||||
.expectedCacheEnabled = false},
|
||||
{.testName = "CacheDisabledWhenCmdNotMatch",
|
||||
.config = R"JSON({
|
||||
"server": {"max_queue_size": 2},
|
||||
"workers": 4,
|
||||
"rpc": {"cache_timeout": 10}
|
||||
})JSON",
|
||||
.method = "server_info2",
|
||||
.isAdmin = false,
|
||||
.expectedCacheEnabled = false},
|
||||
};
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
RPCEngineCache,
|
||||
RPCEngineCacheParameterTest,
|
||||
ValuesIn(generateCacheTestValuesForParametersTest()),
|
||||
tests::util::NameGenerator
|
||||
);
|
||||
|
||||
TEST_P(RPCEngineCacheParameterTest, Test)
|
||||
{
|
||||
auto const& testParam = GetParam();
|
||||
auto const cfgCache = Config{json::parse(testParam.config)};
|
||||
|
||||
auto const admin = testParam.isAdmin;
|
||||
auto const method = testParam.method;
|
||||
std::shared_ptr<RPCEngine<MockLoadBalancer, MockCounters>> engine =
|
||||
RPCEngine<MockLoadBalancer, MockCounters>::make_RPCEngine(
|
||||
cfgCache, backend, mockLoadBalancerPtr, dosGuard, queue, *mockCountersPtr, handlerProvider
|
||||
);
|
||||
int callTime = 2;
|
||||
EXPECT_CALL(*handlerProvider, isClioOnly).Times(callTime).WillRepeatedly(Return(false));
|
||||
if (testParam.expectedCacheEnabled) {
|
||||
EXPECT_CALL(*backend, isTooBusy).WillOnce(Return(false));
|
||||
EXPECT_CALL(*handlerProvider, getHandler).WillOnce(Return(AnyHandler{tests::common::HandlerFake{}}));
|
||||
|
||||
} else {
|
||||
EXPECT_CALL(*backend, isTooBusy).Times(callTime).WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(*handlerProvider, getHandler)
|
||||
.Times(callTime)
|
||||
.WillRepeatedly(Return(AnyHandler{tests::common::HandlerFake{}}));
|
||||
}
|
||||
|
||||
while (callTime-- != 0) {
|
||||
runSpawn([&](auto yield) {
|
||||
auto const ctx = web::Context(
|
||||
yield,
|
||||
method,
|
||||
1,
|
||||
boost::json::parse(R"JSON({"hello": "world", "limit": 50})JSON").as_object(),
|
||||
nullptr,
|
||||
tagFactory,
|
||||
LedgerRange{0, 30},
|
||||
"127.0.0.2",
|
||||
admin
|
||||
);
|
||||
|
||||
auto const res = engine->buildResponse(ctx);
|
||||
auto const response = std::get_if<boost::json::object>(&res.response);
|
||||
EXPECT_TRUE(*response == boost::json::parse(R"JSON({ "computed": "world_50"})JSON").as_object());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RPCEngineTest, NotCacheIfErrorHappen)
|
||||
{
|
||||
auto const cfgCache = Config{json::parse(R"JSON({
|
||||
"server": {"max_queue_size": 2},
|
||||
"workers": 4,
|
||||
"rpc": {"cache_timeout": 10}
|
||||
})JSON")};
|
||||
|
||||
auto const notAdmin = false;
|
||||
auto const method = "server_info";
|
||||
std::shared_ptr<RPCEngine<MockLoadBalancer, MockCounters>> engine =
|
||||
RPCEngine<MockLoadBalancer, MockCounters>::make_RPCEngine(
|
||||
cfgCache, backend, mockLoadBalancerPtr, dosGuard, queue, *mockCountersPtr, handlerProvider
|
||||
);
|
||||
|
||||
int callTime = 2;
|
||||
EXPECT_CALL(*backend, isTooBusy).Times(callTime).WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(*handlerProvider, getHandler)
|
||||
.Times(callTime)
|
||||
.WillRepeatedly(Return(AnyHandler{tests::common::FailingHandlerFake{}}));
|
||||
EXPECT_CALL(*mockCountersPtr, rpcErrored(method)).Times(callTime);
|
||||
EXPECT_CALL(*handlerProvider, isClioOnly).Times(callTime).WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(*handlerProvider, contains).Times(callTime).WillRepeatedly(Return(true));
|
||||
|
||||
while (callTime-- != 0) {
|
||||
runSpawn([&](auto yield) {
|
||||
auto const ctx = web::Context(
|
||||
yield,
|
||||
method,
|
||||
1,
|
||||
boost::json::parse(R"JSON({"hello": "world","limit": 50})JSON").as_object(),
|
||||
nullptr,
|
||||
tagFactory,
|
||||
LedgerRange{0, 30},
|
||||
"127.0.0.2",
|
||||
notAdmin
|
||||
);
|
||||
|
||||
auto const res = engine->buildResponse(ctx);
|
||||
auto const error = std::get_if<rpc::Status>(&res.response);
|
||||
EXPECT_TRUE(*error == rpc::Status{"Very custom error"});
|
||||
});
|
||||
}
|
||||
}
|
||||
70
tests/unit/util/ResponseExpirationCacheTests.cpp
Normal file
70
tests/unit/util/ResponseExpirationCacheTests.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "util/ResponseExpirationCache.hpp"
|
||||
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
using namespace util;
|
||||
|
||||
struct ResponseExpirationCacheTests : public ::testing::Test {
|
||||
protected:
|
||||
ResponseExpirationCache cache_{std::chrono::seconds{100}, {"key"}};
|
||||
boost::json::object object_{{"key", "value"}};
|
||||
};
|
||||
|
||||
TEST_F(ResponseExpirationCacheTests, PutAndGetNotExpired)
|
||||
{
|
||||
EXPECT_FALSE(cache_.get("key").has_value());
|
||||
|
||||
cache_.put("key", object_);
|
||||
auto result = cache_.get("key");
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(*result, object_);
|
||||
result = cache_.get("key2");
|
||||
ASSERT_FALSE(result.has_value());
|
||||
|
||||
cache_.put("key2", object_);
|
||||
result = cache_.get("key2");
|
||||
ASSERT_FALSE(result.has_value());
|
||||
}
|
||||
|
||||
TEST_F(ResponseExpirationCacheTests, Invalidate)
|
||||
{
|
||||
cache_.put("key", object_);
|
||||
cache_.invalidate();
|
||||
EXPECT_FALSE(cache_.get("key").has_value());
|
||||
}
|
||||
|
||||
TEST_F(ResponseExpirationCacheTests, GetExpired)
|
||||
{
|
||||
ResponseExpirationCache cache{std::chrono::milliseconds{1}, {"key"}};
|
||||
auto const response = boost::json::object{{"key", "value"}};
|
||||
|
||||
cache.put("key", response);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{2});
|
||||
|
||||
auto const result = cache.get("key");
|
||||
EXPECT_FALSE(result);
|
||||
}
|
||||
Reference in New Issue
Block a user