Support api_version (#695)

Fixes #64
This commit is contained in:
Alex Kremer
2023-06-16 12:14:30 +01:00
committed by GitHub
parent 871d43c85f
commit a960471ef4
58 changed files with 734 additions and 299 deletions

View File

@@ -153,6 +153,21 @@ TEST_F(ConfigTest, Section)
ASSERT_EQ(sub.value<bool>("bool"), true);
}
TEST_F(ConfigTest, SectionOr)
{
{
auto sub = cfg.sectionOr("section.test", {}); // exists
ASSERT_EQ(sub.value<string>("str"), "hello");
ASSERT_EQ(sub.value<int64_t>("int"), 9042);
ASSERT_EQ(sub.value<bool>("bool"), true);
}
{
auto sub = cfg.sectionOr("section.doesnotexist", {{"int", 9043}}); // does not exist
ASSERT_EQ(sub.value<int64_t>("int"), 9043); // default from fallback
}
}
TEST_F(ConfigTest, Array)
{
auto arr = cfg.array("arr");

View File

@@ -0,0 +1,137 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, 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/Fixtures.h>
#include <rpc/common/impl/APIVersionParser.h>
#include <boost/json/parse.hpp>
#include <fmt/core.h>
#include <gtest/gtest.h>
constexpr static auto DEFAULT_API_VERSION = 5u;
constexpr static auto MIN_API_VERSION = 2u;
constexpr static auto MAX_API_VERSION = 10u;
using namespace RPC::detail;
namespace json = boost::json;
class RPCAPIVersionTest : public NoLoggerFixture
{
protected:
ProductionAPIVersionParser parser{DEFAULT_API_VERSION, MIN_API_VERSION, MAX_API_VERSION};
};
TEST_F(RPCAPIVersionTest, ReturnsDefaultVersionIfNotSpecified)
{
auto ver = parser.parse(json::parse("{}").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), DEFAULT_API_VERSION);
}
TEST_F(RPCAPIVersionTest, ReturnsErrorIfVersionHigherThanMaxSupported)
{
auto ver = parser.parse(json::parse(R"({"api_version": 11})").as_object());
EXPECT_FALSE(ver);
}
TEST_F(RPCAPIVersionTest, ReturnsErrorIfVersionLowerThanMinSupported)
{
auto ver = parser.parse(json::parse(R"({"api_version": 1})").as_object());
EXPECT_FALSE(ver);
}
TEST_F(RPCAPIVersionTest, ReturnsErrorOnWrongType)
{
{
auto ver = parser.parse(json::parse(R"({"api_version": null})").as_object());
EXPECT_FALSE(ver);
}
{
auto ver = parser.parse(json::parse(R"({"api_version": "5"})").as_object());
EXPECT_FALSE(ver);
}
{
auto ver = parser.parse(json::parse(R"({"api_version": "wrong"})").as_object());
EXPECT_FALSE(ver);
}
}
TEST_F(RPCAPIVersionTest, ReturnsParsedVersionIfAllPreconditionsAreMet)
{
{
auto ver = parser.parse(json::parse(R"({"api_version": 2})").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), 2u);
}
{
auto ver = parser.parse(json::parse(R"({"api_version": 10})").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), 10u);
}
{
auto ver = parser.parse(json::parse(R"({"api_version": 5})").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), 5u);
}
}
TEST_F(RPCAPIVersionTest, GetsValuesFromConfigCorrectly)
{
clio::Config cfg{json::parse(fmt::format(
R"({{
"min": {},
"max": {},
"default": {}
}})",
MIN_API_VERSION,
MAX_API_VERSION,
DEFAULT_API_VERSION))};
ProductionAPIVersionParser configuredParser{cfg};
{
auto ver = configuredParser.parse(json::parse(R"({"api_version": 2})").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), 2u);
}
{
auto ver = configuredParser.parse(json::parse(R"({"api_version": 10})").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), 10u);
}
{
auto ver = configuredParser.parse(json::parse(R"({"api_version": 5})").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), 5u);
}
{
auto ver = configuredParser.parse(json::parse(R"({})").as_object());
EXPECT_TRUE(ver);
EXPECT_EQ(ver.value(), DEFAULT_API_VERSION);
}
{
auto ver = configuredParser.parse(json::parse(R"({"api_version": 11})").as_object());
EXPECT_FALSE(ver);
}
{
auto ver = configuredParser.parse(json::parse(R"({"api_version": 1})").as_object());
EXPECT_FALSE(ver);
}
}

View File

@@ -33,47 +33,53 @@ using namespace unittests::detail;
namespace json = boost::json;
class RPCDefaultProcessorTest : public NoLoggerFixture
class RPCDefaultProcessorTest : public HandlerBaseTest
{
};
TEST_F(RPCDefaultProcessorTest, ValidInput)
{
HandlerMock handler;
RPC::detail::DefaultProcessor<HandlerMock> processor;
runSpawn([](auto& yield) {
HandlerMock handler;
RPC::detail::DefaultProcessor<HandlerMock> processor;
auto const input = json::parse(R"({ "something": "works" })");
auto const spec = RpcSpec{{"something", Required{}}};
auto const data = InOutFake{"works"};
EXPECT_CALL(handler, spec()).WillOnce(ReturnRef(spec));
EXPECT_CALL(handler, process(Eq(data))).WillOnce(Return(data));
auto const input = json::parse(R"({ "something": "works" })");
auto const spec = RpcSpec{{"something", Required{}}};
auto const data = InOutFake{"works"};
EXPECT_CALL(handler, spec(_)).WillOnce(ReturnRef(spec));
EXPECT_CALL(handler, process(Eq(data), _)).WillOnce(Return(data));
auto const ret = processor(handler, input);
ASSERT_TRUE(ret); // no error
auto const ret = processor(handler, input, Context{std::ref(yield)});
ASSERT_TRUE(ret); // no error
});
}
TEST_F(RPCDefaultProcessorTest, NoInputVaildCall)
{
HandlerWithoutInputMock handler;
RPC::detail::DefaultProcessor<HandlerWithoutInputMock> processor;
runSpawn([](auto& yield) {
HandlerWithoutInputMock handler;
RPC::detail::DefaultProcessor<HandlerWithoutInputMock> processor;
auto const data = InOutFake{"works"};
auto const input = json::parse(R"({})");
EXPECT_CALL(handler, process()).WillOnce(Return(data));
auto const data = InOutFake{"works"};
auto const input = json::parse(R"({})");
EXPECT_CALL(handler, process(_)).WillOnce(Return(data));
auto const ret = processor(handler, input);
ASSERT_TRUE(ret); // no error
auto const ret = processor(handler, input, Context{std::ref(yield)});
ASSERT_TRUE(ret); // no error
});
}
TEST_F(RPCDefaultProcessorTest, InvalidInput)
{
HandlerMock handler;
RPC::detail::DefaultProcessor<HandlerMock> processor;
runSpawn([](auto& yield) {
HandlerMock handler;
RPC::detail::DefaultProcessor<HandlerMock> processor;
auto const input = json::parse(R"({ "other": "nope" })");
auto const spec = RpcSpec{{"something", Required{}}};
EXPECT_CALL(handler, spec()).WillOnce(ReturnRef(spec));
auto const input = json::parse(R"({ "other": "nope" })");
auto const spec = RpcSpec{{"something", Required{}}};
EXPECT_CALL(handler, spec(_)).WillOnce(ReturnRef(spec));
auto const ret = processor(handler, input);
ASSERT_FALSE(ret); // returns error
auto const ret = processor(handler, input, Context{std::ref(yield)});
ASSERT_FALSE(ret); // returns error
});
}

View File

@@ -36,25 +36,29 @@ class RPCLedgerRangeTest : public HandlerBaseTest
TEST_F(RPCLedgerRangeTest, LedgerRangeMinMaxSame)
{
mockBackendPtr->updateRange(RANGEMIN);
auto const handler = AnyHandler{LedgerRangeHandler{mockBackendPtr}};
auto const req = json::parse("{}");
auto const output = handler.process(req);
ASSERT_TRUE(output);
auto const json = output.value();
EXPECT_EQ(json.at("ledger_index_min").as_uint64(), RANGEMIN);
EXPECT_EQ(json.at("ledger_index_max").as_uint64(), RANGEMIN);
runSpawn([this](auto& yield) {
mockBackendPtr->updateRange(RANGEMIN);
auto const handler = AnyHandler{LedgerRangeHandler{mockBackendPtr}};
auto const req = json::parse("{}");
auto const output = handler.process(req, Context{std::ref(yield)});
ASSERT_TRUE(output);
auto const json = output.value();
EXPECT_EQ(json.at("ledger_index_min").as_uint64(), RANGEMIN);
EXPECT_EQ(json.at("ledger_index_max").as_uint64(), RANGEMIN);
});
}
TEST_F(RPCLedgerRangeTest, LedgerRangeFullySet)
{
mockBackendPtr->updateRange(RANGEMIN);
mockBackendPtr->updateRange(RANGEMAX);
auto const handler = AnyHandler{LedgerRangeHandler{mockBackendPtr}};
auto const req = json::parse("{}");
auto const output = handler.process(req);
ASSERT_TRUE(output);
auto const json = output.value();
EXPECT_EQ(json.at("ledger_index_min").as_uint64(), RANGEMIN);
EXPECT_EQ(json.at("ledger_index_max").as_uint64(), RANGEMAX);
runSpawn([this](auto& yield) {
mockBackendPtr->updateRange(RANGEMIN);
mockBackendPtr->updateRange(RANGEMAX);
auto const handler = AnyHandler{LedgerRangeHandler{mockBackendPtr}};
auto const req = json::parse("{}");
auto const output = handler.process(req, Context{std::ref(yield)});
ASSERT_TRUE(output);
auto const json = output.value();
EXPECT_EQ(json.at("ledger_index_min").as_uint64(), RANGEMIN);
EXPECT_EQ(json.at("ledger_index_max").as_uint64(), RANGEMAX);
});
}

View File

@@ -23,15 +23,17 @@
using namespace RPC;
class RPCPingHandlerTest : public NoLoggerFixture
class RPCPingHandlerTest : public HandlerBaseTest
{
};
// example handler tests
TEST_F(RPCPingHandlerTest, Default)
{
auto const handler = AnyHandler{PingHandler{}};
auto const output = handler.process(boost::json::parse(R"({})"));
ASSERT_TRUE(output);
EXPECT_EQ(output.value(), boost::json::parse(R"({})"));
runSpawn([](auto& yield) {
auto const handler = AnyHandler{PingHandler{}};
auto const output = handler.process(boost::json::parse(R"({})"), Context{std::ref(yield)});
ASSERT_TRUE(output);
EXPECT_EQ(output.value(), boost::json::parse(R"({})"));
});
}

View File

@@ -24,15 +24,17 @@
using namespace RPC;
class RPCRandomHandlerTest : public NoLoggerFixture
class RPCRandomHandlerTest : public HandlerBaseTest
{
};
TEST_F(RPCRandomHandlerTest, Default)
{
auto const handler = AnyHandler{RandomHandler{}};
auto const output = handler.process(boost::json::parse(R"({})"));
ASSERT_TRUE(output);
EXPECT_TRUE(output->as_object().contains(JS(random)));
EXPECT_EQ(output->as_object().at(JS(random)).as_string().size(), 64u);
runSpawn([](auto& yield) {
auto const handler = AnyHandler{RandomHandler{}};
auto const output = handler.process(boost::json::parse(R"({})"), Context{std::ref(yield)});
ASSERT_TRUE(output);
EXPECT_TRUE(output->as_object().contains(JS(random)));
EXPECT_EQ(output->as_object().at(JS(random)).as_string().size(), 64u);
});
}

View File

@@ -31,83 +31,73 @@ using namespace unittests::detail;
namespace json = boost::json;
class RPCTestHandlerTest : public NoLoggerFixture
class RPCTestHandlerTest : public HandlerBaseTest
{
};
// example handler tests
TEST_F(RPCTestHandlerTest, HandlerSuccess)
{
auto const handler = AnyHandler{HandlerFake{}};
auto const input = json::parse(R"({
"hello": "world",
"limit": 10
})");
runSpawn([](auto& yield) {
auto const handler = AnyHandler{HandlerFake{}};
auto const input = json::parse(R"({
"hello": "world",
"limit": 10
})");
auto const output = handler.process(input);
ASSERT_TRUE(output);
auto const val = output.value();
EXPECT_EQ(val.as_object().at("computed").as_string(), "world_10");
}
TEST_F(RPCTestHandlerTest, CoroutineHandlerSuccess)
{
auto const handler = AnyHandler{CoroutineHandlerFake{}};
auto const input = json::parse(R"({
"hello": "world",
"limit": 10
})");
boost::asio::io_context ctx;
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_TRUE(output);
auto const val = output.value();
EXPECT_EQ(val.as_object().at("computed").as_string(), "world_10");
});
ctx.run();
}
TEST_F(RPCTestHandlerTest, NoInputHandlerSuccess)
{
auto const handler = AnyHandler{NoInputHandlerFake{}};
auto const output = handler.process(json::parse(R"({})"));
ASSERT_TRUE(output);
runSpawn([](auto& yield) {
auto const handler = AnyHandler{NoInputHandlerFake{}};
auto const output = handler.process(json::parse(R"({})"), Context{std::ref(yield)});
ASSERT_TRUE(output);
auto const val = output.value();
EXPECT_EQ(val.as_object().at("computed").as_string(), "test");
auto const val = output.value();
EXPECT_EQ(val.as_object().at("computed").as_string(), "test");
});
}
TEST_F(RPCTestHandlerTest, HandlerErrorHandling)
{
auto const handler = AnyHandler{HandlerFake{}};
auto const input = json::parse(R"({
"hello": "not world",
"limit": 10
})");
runSpawn([](auto& yield) {
auto const handler = AnyHandler{HandlerFake{}};
auto const input = json::parse(R"({
"hello": "not world",
"limit": 10
})");
auto const output = handler.process(input);
ASSERT_FALSE(output);
auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output);
auto const err = RPC::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "invalidParams");
EXPECT_EQ(err.at("error_message").as_string(), "Invalid parameters.");
EXPECT_EQ(err.at("error_code").as_uint64(), 31);
auto const err = RPC::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "invalidParams");
EXPECT_EQ(err.at("error_message").as_string(), "Invalid parameters.");
EXPECT_EQ(err.at("error_code").as_uint64(), 31);
});
}
TEST_F(RPCTestHandlerTest, HandlerInnerErrorHandling)
{
auto const handler = AnyHandler{FailingHandlerFake{}};
auto const input = json::parse(R"({
"hello": "world",
"limit": 10
})");
runSpawn([](auto& yield) {
auto const handler = AnyHandler{FailingHandlerFake{}};
auto const input = json::parse(R"({
"hello": "world",
"limit": 10
})");
// validation succeeds but handler itself returns error
auto const output = handler.process(input);
ASSERT_FALSE(output);
// validation succeeds but handler itself returns error
auto const output = handler.process(input, Context{std::ref(yield)});
ASSERT_FALSE(output);
auto const err = RPC::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "Very custom error");
auto const err = RPC::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "Very custom error");
});
}

View File

@@ -73,7 +73,7 @@ public:
using Result = RPC::HandlerReturnType<Output>;
RPC::RpcSpecConstRef
spec() const
spec([[maybe_unused]] uint32_t apiVersion) const
{
using namespace RPC::validation;
@@ -86,35 +86,7 @@ public:
}
Result
process(Input input) const
{
return Output{input.hello + '_' + std::to_string(input.limit.value_or(0))};
}
};
// example handler
class CoroutineHandlerFake
{
public:
using Input = TestInput;
using Output = TestOutput;
using Result = RPC::HandlerReturnType<Output>;
RPC::RpcSpecConstRef
spec() const
{
using namespace RPC::validation;
static const auto rpcSpec = RPC::RpcSpec{
{"hello", Required{}, Type<std::string>{}, EqualTo{"world"}},
{"limit", Type<uint32_t>{}, Between<uint32_t>{0, 100}}, // optional field
};
return rpcSpec;
}
Result
process(Input input, RPC::Context const& ctx) const
process(Input input, [[maybe_unused]] RPC::Context const& ctx) const
{
return Output{input.hello + '_' + std::to_string(input.limit.value_or(0))};
}
@@ -127,7 +99,7 @@ public:
using Result = RPC::HandlerReturnType<Output>;
Result
process() const
process([[maybe_unused]] RPC::Context const& ctx) const
{
return Output{"test"};
}
@@ -142,7 +114,7 @@ public:
using Result = RPC::HandlerReturnType<Output>;
RPC::RpcSpecConstRef
spec() const
spec([[maybe_unused]] uint32_t apiVersion) const
{
using namespace RPC::validation;
@@ -155,7 +127,7 @@ public:
}
Result
process([[maybe_unused]] Input input) const
process([[maybe_unused]] Input input, [[maybe_unused]] RPC::Context const& ctx) const
{
// always fail
return RPC::Error{RPC::Status{"Very custom error"}};
@@ -191,8 +163,8 @@ struct HandlerMock
using Output = InOutFake;
using Result = RPC::HandlerReturnType<Output>;
MOCK_METHOD(RPC::RpcSpecConstRef, spec, (), (const));
MOCK_METHOD(Result, process, (Input), (const));
MOCK_METHOD(RPC::RpcSpecConstRef, spec, (uint32_t), (const));
MOCK_METHOD(Result, process, (Input, RPC::Context const&), (const));
};
struct HandlerWithoutInputMock
@@ -200,7 +172,7 @@ struct HandlerWithoutInputMock
using Output = InOutFake;
using Result = RPC::HandlerReturnType<Output>;
MOCK_METHOD(Result, process, (), (const));
MOCK_METHOD(Result, process, (RPC::Context const&), (const));
};
} // namespace unittests::detail

View File

@@ -378,6 +378,71 @@ TEST_F(WebRPCExecutorTest, WsNotReady)
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, HTTPInvalidAPIVersion)
{
static auto constexpr request = R"({
"method": "server_info",
"params": [{
"api_version": null
}]
})";
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr response = R"({
"result": {
"error": "invalid_API_version",
"error_code": 6000,
"error_message": "API version must be an integer",
"status": "error",
"type": "response",
"request": {
"method": "server_info",
"params": [{
"api_version": null
}]
}
}
})";
EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1);
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, WSInvalidAPIVersion)
{
session->upgraded = true;
static auto constexpr request = R"({
"method": "server_info",
"api_version": null
})";
mockBackendPtr->updateRange(MINSEQ); // min
mockBackendPtr->updateRange(MAXSEQ); // max
static auto constexpr response = R"({
"error": "invalid_API_version",
"error_code": 6000,
"error_message": "API version must be an integer",
"status": "error",
"type": "response",
"request": {
"method": "server_info",
"api_version": null
}
})";
EXPECT_CALL(*rpcEngine, notifyBadSyntax).Times(1);
(*rpcExecutor)(std::move(request), session);
std::this_thread::sleep_for(200ms);
EXPECT_EQ(boost::json::parse(session->message), boost::json::parse(response));
}
TEST_F(WebRPCExecutorTest, HTTPBadSyntax)
{
static auto constexpr request = R"({"method2": "server_info"})";
@@ -389,7 +454,7 @@ TEST_F(WebRPCExecutorTest, HTTPBadSyntax)
"result":{
"error": "badSyntax",
"error_code": 1,
"error_message": "Syntax error.",
"error_message": "Method is not specified or is not a string.",
"status": "error",
"type": "response",
"request": {
@@ -417,7 +482,7 @@ TEST_F(WebRPCExecutorTest, HTTPBadSyntaxWhenRequestSubscribe)
"result": {
"error": "badSyntax",
"error_code": 1,
"error_message": "Syntax error.",
"error_message": "Subscribe and unsubscribe are only allowed or websocket.",
"status": "error",
"type": "response",
"request": {
@@ -448,7 +513,7 @@ TEST_F(WebRPCExecutorTest, WsBadSyntax)
static auto constexpr response = R"({
"error": "badSyntax",
"error_code": 1,
"error_message": "Syntax error.",
"error_message": "Method/Command is not specified or is not a string.",
"status": "error",
"type": "response",
"id": 99,