mirror of
				https://github.com/XRPLF/clio.git
				synced 2025-11-04 11:55:51 +00:00 
			
		
		
		
	@@ -68,6 +68,7 @@ target_sources(clio PRIVATE
 | 
			
		||||
  src/rpc/WorkQueue.cpp
 | 
			
		||||
  src/rpc/common/Specs.cpp
 | 
			
		||||
  src/rpc/common/Validators.cpp
 | 
			
		||||
  src/rpc/common/impl/APIVersionParser.cpp
 | 
			
		||||
  # RPC impl
 | 
			
		||||
  src/rpc/common/impl/HandlerProvider.cpp
 | 
			
		||||
  ## RPC handler
 | 
			
		||||
@@ -125,6 +126,7 @@ if(BUILD_TESTS)
 | 
			
		||||
    unittests/rpc/RPCHelpersTest.cpp
 | 
			
		||||
    unittests/rpc/CountersTest.cpp
 | 
			
		||||
    unittests/rpc/AdminVerificationTest.cpp
 | 
			
		||||
    unittests/rpc/APIVersionTests.cpp
 | 
			
		||||
    ## RPC handlers
 | 
			
		||||
    unittests/rpc/handlers/DefaultProcessorTests.cpp
 | 
			
		||||
    unittests/rpc/handlers/TestHandlerTests.cpp
 | 
			
		||||
@@ -167,9 +169,11 @@ if(BUILD_TESTS)
 | 
			
		||||
    unittests/webserver/RPCExecutorTest.cpp)
 | 
			
		||||
  include(CMake/deps/gtest.cmake)
 | 
			
		||||
 | 
			
		||||
  # test for dwarf5 bug on ci 
 | 
			
		||||
  # fix for dwarf5 bug on ci 
 | 
			
		||||
  target_compile_options(clio PUBLIC -gdwarf-4)
 | 
			
		||||
 | 
			
		||||
  target_compile_definitions(${TEST_TARGET} PUBLIC UNITTEST_BUILD)
 | 
			
		||||
 | 
			
		||||
  # if CODE_COVERAGE enable, add clio_test-ccov
 | 
			
		||||
  if(CODE_COVERAGE)
 | 
			
		||||
    include(CMake/coverage.cmake)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								README.md
									
									
									
									
									
								
							@@ -96,12 +96,12 @@ The parameters `ssl_cert_file` and `ssl_key_file` can also be added to the top l
 | 
			
		||||
An example of how to specify `ssl_cert_file` and `ssl_key_file` in the config:
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
"server":{
 | 
			
		||||
"server": {
 | 
			
		||||
    "ip": "0.0.0.0",
 | 
			
		||||
    "port": 51233
 | 
			
		||||
},
 | 
			
		||||
"ssl_cert_file" : "/full/path/to/cert.file",
 | 
			
		||||
"ssl_key_file" : "/full/path/to/key.file"
 | 
			
		||||
"ssl_cert_file": "/full/path/to/cert.file",
 | 
			
		||||
"ssl_key_file": "/full/path/to/key.file"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Once your config files are ready, start rippled and Clio. It doesn't matter which you
 | 
			
		||||
@@ -172,6 +172,20 @@ which can cause high latencies. A possible alternative to this is to just deploy
 | 
			
		||||
a database in each region, and the Clio nodes in each region use their region's database.
 | 
			
		||||
This is effectively two systems.
 | 
			
		||||
 | 
			
		||||
Clio supports API versioning as [described here](https://xrpl.org/request-formatting.html#api-versioning).
 | 
			
		||||
It's possible to configure `minimum`, `maximum` and `default` version like so: 
 | 
			
		||||
```json
 | 
			
		||||
"api_version": {
 | 
			
		||||
    "min": 1,
 | 
			
		||||
    "max": 2,
 | 
			
		||||
    "default": 2 
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
All of the above are optional. 
 | 
			
		||||
Clio will fallback to hardcoded defaults when not specified in the config file or configured values are outside 
 | 
			
		||||
of the minimum and maximum supported versions hardcoded in `src/rpc/common/APIVersion.h`.
 | 
			
		||||
> **Note:** See `example-config.json` for more details. 
 | 
			
		||||
 | 
			
		||||
## Developing against `rippled` in standalone mode
 | 
			
		||||
 | 
			
		||||
If you wish you develop against a `rippled` instance running in standalone
 | 
			
		||||
 
 | 
			
		||||
@@ -90,4 +90,9 @@
 | 
			
		||||
    //"finish_sequence": [integer] the ledger index to finish at,
 | 
			
		||||
    //"ssl_cert_file" : "/full/path/to/cert.file",
 | 
			
		||||
    //"ssl_key_file" : "/full/path/to/key.file"
 | 
			
		||||
    "api_version": {
 | 
			
		||||
        "min": 2,
 | 
			
		||||
        "max": 2,
 | 
			
		||||
        "default": 2 // Clio only supports API v2 and newer
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -142,6 +142,15 @@ Config::section(key_type key) const
 | 
			
		||||
    throw std::logic_error("No section found at '" + key + "'");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Config
 | 
			
		||||
Config::sectionOr(key_type key, boost::json::object fallback) const
 | 
			
		||||
{
 | 
			
		||||
    auto maybe_element = lookup(key);
 | 
			
		||||
    if (maybe_element && maybe_element->is_object())
 | 
			
		||||
        return Config{std::move(*maybe_element)};
 | 
			
		||||
    return Config{std::move(fallback)};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Config::array_type
 | 
			
		||||
Config::array() const
 | 
			
		||||
{
 | 
			
		||||
 
 | 
			
		||||
@@ -266,6 +266,20 @@ public:
 | 
			
		||||
    [[nodiscard]] Config
 | 
			
		||||
    section(key_type key) const;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Interface for fetching a sub section by key with a fallback object.
 | 
			
		||||
     *
 | 
			
		||||
     * Will attempt to fetch an entire section under the desired key and return
 | 
			
		||||
     * it as a Config instance. If the section does not exist or another type is
 | 
			
		||||
     * stored under the desired key - fallback object is used instead.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The key to check
 | 
			
		||||
     * @param fallabkc The fallback object
 | 
			
		||||
     * @return Config Section represented as a separate instance of Config
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] Config
 | 
			
		||||
    sectionOr(key_type key, boost::json::object fallback) const;
 | 
			
		||||
 | 
			
		||||
    //
 | 
			
		||||
    // Direct self-value access
 | 
			
		||||
    //
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include <rpc/Errors.h>
 | 
			
		||||
#include <rpc/JS.h>
 | 
			
		||||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
 | 
			
		||||
@@ -77,7 +78,8 @@ getErrorInfo(ClioError code)
 | 
			
		||||
        {ClioError::rpcMALFORMED_REQUEST, "malformedRequest", "Malformed request."},
 | 
			
		||||
        {ClioError::rpcMALFORMED_OWNER, "malformedOwner", "Malformed owner."},
 | 
			
		||||
        {ClioError::rpcMALFORMED_ADDRESS, "malformedAddress", "Malformed address."},
 | 
			
		||||
        {ClioError::rpcINVALID_HOT_WALLET, "invalidHotWallet", "Invalid hot wallet."}};
 | 
			
		||||
        {ClioError::rpcINVALID_HOT_WALLET, "invalidHotWallet", "Invalid hot wallet."},
 | 
			
		||||
        {ClioError::rpcINVALID_API_VERSION, JS(invalid_API_version), "Invalid API version."}};
 | 
			
		||||
 | 
			
		||||
    auto matchByCode = [code](auto const& info) { return info.code == code; };
 | 
			
		||||
    if (auto it = find_if(begin(infos), end(infos), matchByCode); it != end(infos))
 | 
			
		||||
 
 | 
			
		||||
@@ -34,11 +34,15 @@ namespace RPC {
 | 
			
		||||
 * @brief Custom clio RPC Errors.
 | 
			
		||||
 */
 | 
			
		||||
enum class ClioError {
 | 
			
		||||
    // normal clio errors start with 5000
 | 
			
		||||
    rpcMALFORMED_CURRENCY = 5000,
 | 
			
		||||
    rpcMALFORMED_REQUEST = 5001,
 | 
			
		||||
    rpcMALFORMED_OWNER = 5002,
 | 
			
		||||
    rpcMALFORMED_ADDRESS = 5003,
 | 
			
		||||
    rpcINVALID_HOT_WALLET = 5004,
 | 
			
		||||
 | 
			
		||||
    // special system errors start with 6000
 | 
			
		||||
    rpcINVALID_API_VERSION = 6000,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -18,21 +18,25 @@
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include <rpc/Factories.h>
 | 
			
		||||
#include <rpc/common/Types.h>
 | 
			
		||||
 | 
			
		||||
using namespace std;
 | 
			
		||||
using namespace clio;
 | 
			
		||||
 | 
			
		||||
namespace RPC {
 | 
			
		||||
 | 
			
		||||
optional<Web::Context>
 | 
			
		||||
util::Expected<Web::Context, Status>
 | 
			
		||||
make_WsContext(
 | 
			
		||||
    boost::asio::yield_context& yc,
 | 
			
		||||
    boost::json::object const& request,
 | 
			
		||||
    shared_ptr<Server::ConnectionBase> const& session,
 | 
			
		||||
    util::TagDecoratorFactory const& tagFactory,
 | 
			
		||||
    Backend::LedgerRange const& range,
 | 
			
		||||
    string const& clientIp)
 | 
			
		||||
    string const& clientIp,
 | 
			
		||||
    std::reference_wrapper<APIVersionParser const> apiVersionParser)
 | 
			
		||||
{
 | 
			
		||||
    using Error = util::Unexpected<Status>;
 | 
			
		||||
 | 
			
		||||
    boost::json::value commandValue = nullptr;
 | 
			
		||||
    if (!request.contains("command") && request.contains("method"))
 | 
			
		||||
        commandValue = request.at("method");
 | 
			
		||||
@@ -40,40 +44,48 @@ make_WsContext(
 | 
			
		||||
        commandValue = request.at("command");
 | 
			
		||||
 | 
			
		||||
    if (!commandValue.is_string())
 | 
			
		||||
        return {};
 | 
			
		||||
        return Error{{RippledError::rpcBAD_SYNTAX, "Method/Command is not specified or is not a string."}};
 | 
			
		||||
 | 
			
		||||
    auto const apiVersion = apiVersionParser.get().parse(request);
 | 
			
		||||
    if (!apiVersion)
 | 
			
		||||
        return Error{{ClioError::rpcINVALID_API_VERSION, apiVersion.error()}};
 | 
			
		||||
 | 
			
		||||
    string command = commandValue.as_string().c_str();
 | 
			
		||||
    return make_optional<Web::Context>(yc, command, 1, request, session, tagFactory, range, clientIp);
 | 
			
		||||
    return Web::Context(yc, command, *apiVersion, request, session, tagFactory, range, clientIp);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
optional<Web::Context>
 | 
			
		||||
util::Expected<Web::Context, Status>
 | 
			
		||||
make_HttpContext(
 | 
			
		||||
    boost::asio::yield_context& yc,
 | 
			
		||||
    boost::json::object const& request,
 | 
			
		||||
    util::TagDecoratorFactory const& tagFactory,
 | 
			
		||||
    Backend::LedgerRange const& range,
 | 
			
		||||
    string const& clientIp)
 | 
			
		||||
    string const& clientIp,
 | 
			
		||||
    std::reference_wrapper<APIVersionParser const> apiVersionParser)
 | 
			
		||||
{
 | 
			
		||||
    using Error = util::Unexpected<Status>;
 | 
			
		||||
 | 
			
		||||
    if (!request.contains("method") || !request.at("method").is_string())
 | 
			
		||||
        return {};
 | 
			
		||||
        return Error{{RippledError::rpcBAD_SYNTAX, "Method is not specified or is not a string."}};
 | 
			
		||||
 | 
			
		||||
    string const& command = request.at("method").as_string().c_str();
 | 
			
		||||
 | 
			
		||||
    if (command == "subscribe" || command == "unsubscribe")
 | 
			
		||||
        return {};
 | 
			
		||||
        return Error{{RippledError::rpcBAD_SYNTAX, "Subscribe and unsubscribe are only allowed or websocket."}};
 | 
			
		||||
 | 
			
		||||
    if (!request.at("params").is_array())
 | 
			
		||||
        return {};
 | 
			
		||||
        return Error{{RippledError::rpcBAD_SYNTAX, "Missing params array."}};
 | 
			
		||||
 | 
			
		||||
    boost::json::array const& array = request.at("params").as_array();
 | 
			
		||||
 | 
			
		||||
    if (array.size() != 1)
 | 
			
		||||
        return {};
 | 
			
		||||
    if (array.size() != 1 || !array.at(0).is_object())
 | 
			
		||||
        return Error{{RippledError::rpcBAD_SYNTAX, "Params must be an array holding exactly one object."}};
 | 
			
		||||
 | 
			
		||||
    if (!array.at(0).is_object())
 | 
			
		||||
        return {};
 | 
			
		||||
    auto const apiVersion = apiVersionParser.get().parse(request.at("params").as_array().at(0).as_object());
 | 
			
		||||
    if (!apiVersion)
 | 
			
		||||
        return Error{{ClioError::rpcINVALID_API_VERSION, apiVersion.error()}};
 | 
			
		||||
 | 
			
		||||
    return make_optional<Web::Context>(yc, command, 1, array.at(0).as_object(), nullptr, tagFactory, range, clientIp);
 | 
			
		||||
    return Web::Context(yc, command, *apiVersion, array.at(0).as_object(), nullptr, tagFactory, range, clientIp);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace RPC
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,9 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <backend/BackendInterface.h>
 | 
			
		||||
#include <rpc/Errors.h>
 | 
			
		||||
#include <rpc/common/APIVersion.h>
 | 
			
		||||
#include <util/Expected.h>
 | 
			
		||||
#include <webserver/Context.h>
 | 
			
		||||
#include <webserver/interface/ConnectionBase.h>
 | 
			
		||||
 | 
			
		||||
@@ -38,21 +41,23 @@
 | 
			
		||||
 */
 | 
			
		||||
namespace RPC {
 | 
			
		||||
 | 
			
		||||
std::optional<Web::Context>
 | 
			
		||||
util::Expected<Web::Context, Status>
 | 
			
		||||
make_WsContext(
 | 
			
		||||
    boost::asio::yield_context& yc,
 | 
			
		||||
    boost::json::object const& request,
 | 
			
		||||
    std::shared_ptr<Server::ConnectionBase> const& session,
 | 
			
		||||
    util::TagDecoratorFactory const& tagFactory,
 | 
			
		||||
    Backend::LedgerRange const& range,
 | 
			
		||||
    std::string const& clientIp);
 | 
			
		||||
    std::string const& clientIp,
 | 
			
		||||
    std::reference_wrapper<APIVersionParser const> apiVersionParser);
 | 
			
		||||
 | 
			
		||||
std::optional<Web::Context>
 | 
			
		||||
util::Expected<Web::Context, Status>
 | 
			
		||||
make_HttpContext(
 | 
			
		||||
    boost::asio::yield_context& yc,
 | 
			
		||||
    boost::json::object const& request,
 | 
			
		||||
    util::TagDecoratorFactory const& tagFactory,
 | 
			
		||||
    Backend::LedgerRange const& range,
 | 
			
		||||
    std::string const& clientIp);
 | 
			
		||||
    std::string const& clientIp,
 | 
			
		||||
    std::reference_wrapper<APIVersionParser const> apiVersionParser);
 | 
			
		||||
 | 
			
		||||
}  // namespace RPC
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,6 @@ Handlers need to fulfil the requirements specified by the **`Handler`** concept
 | 
			
		||||
- Expose types: 
 | 
			
		||||
	* `Input` - The POD struct which acts as input for the handler
 | 
			
		||||
	* `Output` - The POD struct which acts as output of a valid handler invocation
 | 
			
		||||
- Have a `spec()` member function returning a const reference to an **`RpcSpec`** describing the JSON input.
 | 
			
		||||
- Have a `spec(uint32_t)` member function returning a const reference to an **`RpcSpec`** describing the JSON input.
 | 
			
		||||
- Have a `process(Input)` member function that operates on `Input` POD and returns `HandlerReturnType<Output>`
 | 
			
		||||
- Implement `value_from` and `value_to` support using `tag_invoke` as per `boost::json` documentation for these functions.
 | 
			
		||||
 
 | 
			
		||||
@@ -148,7 +148,7 @@ public:
 | 
			
		||||
            perfLog_.debug() << ctx.tag() << " start executing rpc `" << ctx.method << '`';
 | 
			
		||||
 | 
			
		||||
            auto const isAdmin = adminVerifier_.isAdmin(ctx.clientIp);
 | 
			
		||||
            auto const context = Context{ctx.yield, ctx.session, isAdmin, ctx.clientIp};
 | 
			
		||||
            auto const context = Context{ctx.yield, ctx.session, isAdmin, ctx.clientIp, ctx.apiVersion};
 | 
			
		||||
            auto const v = (*method).process(ctx.params, context);
 | 
			
		||||
 | 
			
		||||
            perfLog_.debug() << ctx.tag() << " finish executing rpc `" << ctx.method << '`';
 | 
			
		||||
@@ -303,7 +303,7 @@ private:
 | 
			
		||||
    bool
 | 
			
		||||
    shouldForwardToRippled(Web::Context const& ctx) const
 | 
			
		||||
    {
 | 
			
		||||
        auto request = ctx.params;
 | 
			
		||||
        auto const& request = ctx.params;
 | 
			
		||||
 | 
			
		||||
        if (isClioOnly(ctx.method))
 | 
			
		||||
            return false;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										59
									
								
								src/rpc/common/APIVersion.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/rpc/common/APIVersion.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <rpc/common/Types.h>
 | 
			
		||||
#include <util/Expected.h>
 | 
			
		||||
 | 
			
		||||
#include <boost/json.hpp>
 | 
			
		||||
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
namespace RPC {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Default API version to use if no version is specified by clients
 | 
			
		||||
 */
 | 
			
		||||
static constexpr uint32_t API_VERSION_DEFAULT = 2u;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Minimum API version supported by this build
 | 
			
		||||
 *
 | 
			
		||||
 * Note: Clio does not support v1 and only supports v2 and newer.
 | 
			
		||||
 */
 | 
			
		||||
static constexpr uint32_t API_VERSION_MIN = 2u;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Maximum API version supported by this build
 | 
			
		||||
 */
 | 
			
		||||
static constexpr uint32_t API_VERSION_MAX = 2u;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief A baseclass for API version helper
 | 
			
		||||
 */
 | 
			
		||||
class APIVersionParser
 | 
			
		||||
{
 | 
			
		||||
public:
 | 
			
		||||
    virtual ~APIVersionParser() = default;
 | 
			
		||||
 | 
			
		||||
    util::Expected<uint32_t, std::string> virtual parse(boost::json::object const& request) const = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace RPC
 | 
			
		||||
@@ -69,18 +69,7 @@ public:
 | 
			
		||||
     * @brief Process incoming JSON by the stored handler
 | 
			
		||||
     *
 | 
			
		||||
     * @param value The JSON to process
 | 
			
		||||
     * @return JSON result or @ref Status on error
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] ReturnType
 | 
			
		||||
    process(boost::json::value const& value) const
 | 
			
		||||
    {
 | 
			
		||||
        return pimpl_->process(value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Process incoming JSON by the stored handler in a provided coroutine
 | 
			
		||||
     *
 | 
			
		||||
     * @param value The JSON to process
 | 
			
		||||
     * @param ctx Request context
 | 
			
		||||
     * @return JSON result or @ref Status on error
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] ReturnType
 | 
			
		||||
@@ -97,9 +86,6 @@ private:
 | 
			
		||||
        [[nodiscard]] virtual ReturnType
 | 
			
		||||
        process(boost::json::value const& value, Context const& ctx) const = 0;
 | 
			
		||||
 | 
			
		||||
        [[nodiscard]] virtual ReturnType
 | 
			
		||||
        process(boost::json::value const& value) const = 0;
 | 
			
		||||
 | 
			
		||||
        [[nodiscard]] virtual std::unique_ptr<Concept>
 | 
			
		||||
        clone() const = 0;
 | 
			
		||||
    };
 | 
			
		||||
@@ -114,16 +100,10 @@ private:
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [[nodiscard]] ReturnType
 | 
			
		||||
        process(boost::json::value const& value) const override
 | 
			
		||||
        {
 | 
			
		||||
            return processor(handler, value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [[nodiscard]] ReturnType
 | 
			
		||||
        process(boost::json::value const& value, Context const& ctx) const override
 | 
			
		||||
        {
 | 
			
		||||
            return processor(handler, value, &ctx);
 | 
			
		||||
            return processor(handler, value, ctx);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [[nodiscard]] std::unique_ptr<Concept>
 | 
			
		||||
 
 | 
			
		||||
@@ -59,22 +59,14 @@ concept ContextProcessWithoutInput = requires(T a, typename T::Output out, Conte
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template <typename T>
 | 
			
		||||
concept NonContextProcess = requires(T a, typename T::Input in, typename T::Output out) {
 | 
			
		||||
    { a.process(in) } -> std::same_as<HandlerReturnType<decltype(out)>>; 
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template <typename T>
 | 
			
		||||
concept HandlerWithInput = requires(T a) {
 | 
			
		||||
    { a.spec() } -> std::same_as<RpcSpecConstRef>; 
 | 
			
		||||
concept HandlerWithInput = requires(T a, uint32_t version) {
 | 
			
		||||
    { a.spec(version) } -> std::same_as<RpcSpecConstRef>; 
 | 
			
		||||
}
 | 
			
		||||
and (ContextProcessWithInput<T> or NonContextProcess<T>)
 | 
			
		||||
and ContextProcessWithInput<T>
 | 
			
		||||
and boost::json::has_value_to<typename T::Input>::value;
 | 
			
		||||
 | 
			
		||||
template <typename T>
 | 
			
		||||
concept HandlerWithoutInput = requires(T a, typename T::Output out) {
 | 
			
		||||
    { a.process() } -> std::same_as<HandlerReturnType<decltype(out)>>; 
 | 
			
		||||
} 
 | 
			
		||||
or ContextProcessWithoutInput<T>;
 | 
			
		||||
concept HandlerWithoutInput = ContextProcessWithoutInput<T>;
 | 
			
		||||
 | 
			
		||||
template <typename T>
 | 
			
		||||
concept Handler = 
 | 
			
		||||
 
 | 
			
		||||
@@ -73,6 +73,7 @@ struct Context
 | 
			
		||||
    std::shared_ptr<Server::ConnectionBase> session;
 | 
			
		||||
    bool isAdmin = false;
 | 
			
		||||
    std::string clientIp;
 | 
			
		||||
    uint32_t apiVersion = 0u;  // invalid by default
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
using Result = std::variant<Status, boost::json::object>;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										61
									
								
								src/rpc/common/impl/APIVersionParser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/rpc/common/impl/APIVersionParser.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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 <log/Logger.h>
 | 
			
		||||
#include <rpc/common/impl/APIVersionParser.h>
 | 
			
		||||
 | 
			
		||||
#include <fmt/core.h>
 | 
			
		||||
 | 
			
		||||
using namespace std;
 | 
			
		||||
 | 
			
		||||
namespace RPC::detail {
 | 
			
		||||
 | 
			
		||||
ProductionAPIVersionParser::ProductionAPIVersionParser(clio::Config const& config)
 | 
			
		||||
    : ProductionAPIVersionParser(
 | 
			
		||||
          config.valueOr("default", API_VERSION_DEFAULT),
 | 
			
		||||
          config.valueOr("min", API_VERSION_MIN),
 | 
			
		||||
          config.valueOr("max", API_VERSION_MAX))
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
util::Expected<uint32_t, std::string>
 | 
			
		||||
ProductionAPIVersionParser::parse(boost::json::object const& request) const
 | 
			
		||||
{
 | 
			
		||||
    using Error = util::Unexpected<std::string>;
 | 
			
		||||
 | 
			
		||||
    if (request.contains("api_version"))
 | 
			
		||||
    {
 | 
			
		||||
        if (!request.at("api_version").is_int64())
 | 
			
		||||
            return Error{"API version must be an integer"};
 | 
			
		||||
 | 
			
		||||
        auto const version = request.at("api_version").as_int64();
 | 
			
		||||
 | 
			
		||||
        if (version > maxVersion_)
 | 
			
		||||
            return Error{fmt::format("Requested API version is higher than maximum supported ({})", maxVersion_)};
 | 
			
		||||
 | 
			
		||||
        if (version < minVersion_)
 | 
			
		||||
            return Error{fmt::format("Requested API version is lower than minimum supported ({})", minVersion_)};
 | 
			
		||||
 | 
			
		||||
        return version;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return defaultVersion_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace RPC::detail
 | 
			
		||||
							
								
								
									
										79
									
								
								src/rpc/common/impl/APIVersionParser.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/rpc/common/impl/APIVersionParser.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    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.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <config/Config.h>
 | 
			
		||||
#include <log/Logger.h>
 | 
			
		||||
#include <rpc/common/APIVersion.h>
 | 
			
		||||
#include <util/Expected.h>
 | 
			
		||||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
namespace RPC::detail {
 | 
			
		||||
 | 
			
		||||
class ProductionAPIVersionParser : public APIVersionParser
 | 
			
		||||
{
 | 
			
		||||
    clio::Logger log_{"RPC"};
 | 
			
		||||
 | 
			
		||||
    uint32_t defaultVersion_;
 | 
			
		||||
    uint32_t minVersion_;
 | 
			
		||||
    uint32_t maxVersion_;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    // Note: this constructor must remain in the header because of UNITTEST_BUILD definition below
 | 
			
		||||
    ProductionAPIVersionParser(
 | 
			
		||||
        uint32_t defaultVersion = API_VERSION_DEFAULT,
 | 
			
		||||
        uint32_t minVersion = API_VERSION_MIN,
 | 
			
		||||
        uint32_t maxVersion = API_VERSION_MAX)
 | 
			
		||||
        : defaultVersion_{defaultVersion}, minVersion_{minVersion}, maxVersion_{maxVersion}
 | 
			
		||||
    {
 | 
			
		||||
#ifndef UNITTEST_BUILD
 | 
			
		||||
        // in production, we don't want the ability to misconfigure clio with bogus versions
 | 
			
		||||
        // that are not actually supported by the code itself. for testing it is desired however.
 | 
			
		||||
        auto checkRange = [this](uint32_t version, std::string label) {
 | 
			
		||||
            if (std::clamp(version, API_VERSION_MIN, API_VERSION_MAX) != version)
 | 
			
		||||
            {
 | 
			
		||||
                log_.error() << "API version settings issue detected: " << label << " version with value " << version
 | 
			
		||||
                             << " is outside of supported range " << API_VERSION_MIN << "-" << API_VERSION_MAX
 | 
			
		||||
                             << "; Falling back to hardcoded values.";
 | 
			
		||||
 | 
			
		||||
                defaultVersion_ = API_VERSION_DEFAULT;
 | 
			
		||||
                minVersion_ = API_VERSION_MIN;
 | 
			
		||||
                maxVersion_ = API_VERSION_MAX;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        checkRange(defaultVersion, "default");
 | 
			
		||||
        checkRange(minVersion, "minimum");
 | 
			
		||||
        checkRange(maxVersion, "maximum");
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
        log_.info() << "API version settings: [min = " << minVersion_ << "; max = " << maxVersion_
 | 
			
		||||
                    << "; default = " << defaultVersion_ << "]";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ProductionAPIVersionParser(clio::Config const& config);
 | 
			
		||||
 | 
			
		||||
    util::Expected<uint32_t, std::string>
 | 
			
		||||
    parse(boost::json::object const& request) const override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace RPC::detail
 | 
			
		||||
@@ -43,11 +43,7 @@ class ProductionHandlerProvider final : public HandlerProvider
 | 
			
		||||
    struct Handler
 | 
			
		||||
    {
 | 
			
		||||
        AnyHandler handler;
 | 
			
		||||
        bool isClioOnly;
 | 
			
		||||
 | 
			
		||||
        /* implicit */ Handler(AnyHandler handler, bool clioOnly = false) : handler{handler}, isClioOnly{clioOnly}
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        bool isClioOnly = false;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    std::unordered_map<std::string, Handler> handlerMap_;
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <rpc/common/APIVersion.h>
 | 
			
		||||
#include <rpc/common/Concepts.h>
 | 
			
		||||
#include <rpc/common/Types.h>
 | 
			
		||||
 | 
			
		||||
@@ -31,56 +32,35 @@ template <Handler HandlerType>
 | 
			
		||||
struct DefaultProcessor final
 | 
			
		||||
{
 | 
			
		||||
    [[nodiscard]] ReturnType
 | 
			
		||||
    operator()(HandlerType const& handler, boost::json::value const& value, Context const* ctx = nullptr) const
 | 
			
		||||
    operator()(HandlerType const& handler, boost::json::value const& value, Context const& ctx) const
 | 
			
		||||
    {
 | 
			
		||||
        using boost::json::value_from;
 | 
			
		||||
        using boost::json::value_to;
 | 
			
		||||
        if constexpr (HandlerWithInput<HandlerType>)
 | 
			
		||||
        {
 | 
			
		||||
            // first we run validation
 | 
			
		||||
            auto const spec = handler.spec();
 | 
			
		||||
            // first we run validation against specified API version
 | 
			
		||||
            auto const spec = handler.spec(ctx.apiVersion);
 | 
			
		||||
            if (auto const ret = spec.validate(value); not ret)
 | 
			
		||||
                return Error{ret.error()};  // forward Status
 | 
			
		||||
 | 
			
		||||
            auto const inData = value_to<typename HandlerType::Input>(value);
 | 
			
		||||
            if constexpr (NonContextProcess<HandlerType>)
 | 
			
		||||
            {
 | 
			
		||||
                auto const ret = handler.process(inData);
 | 
			
		||||
                // real handler is given expected Input, not json
 | 
			
		||||
                if (!ret)
 | 
			
		||||
                    return Error{ret.error()};  // forward Status
 | 
			
		||||
                else
 | 
			
		||||
                    return value_from(ret.value());
 | 
			
		||||
            }
 | 
			
		||||
            auto const ret = handler.process(inData, ctx);
 | 
			
		||||
 | 
			
		||||
            // real handler is given expected Input, not json
 | 
			
		||||
            if (!ret)
 | 
			
		||||
                return Error{ret.error()};  // forward Status
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                auto const ret = handler.process(inData, *ctx);
 | 
			
		||||
                // real handler is given expected Input, not json
 | 
			
		||||
                if (!ret)
 | 
			
		||||
                    return Error{ret.error()};  // forward Status
 | 
			
		||||
                else
 | 
			
		||||
                    return value_from(ret.value());
 | 
			
		||||
            }
 | 
			
		||||
                return value_from(ret.value());
 | 
			
		||||
        }
 | 
			
		||||
        else if constexpr (HandlerWithoutInput<HandlerType>)
 | 
			
		||||
        {
 | 
			
		||||
            using OutType = HandlerReturnType<typename HandlerType::Output>;
 | 
			
		||||
 | 
			
		||||
            // no input to pass, ignore the value
 | 
			
		||||
            if constexpr (ContextProcessWithoutInput<HandlerType>)
 | 
			
		||||
            {
 | 
			
		||||
                if (auto const ret = handler.process(*ctx); not ret)
 | 
			
		||||
                    return Error{ret.error()};  // forward Status
 | 
			
		||||
                else
 | 
			
		||||
                    return value_from(ret.value());
 | 
			
		||||
            }
 | 
			
		||||
            if (auto const ret = handler.process(ctx); not ret)
 | 
			
		||||
                return Error{ret.error()};  // forward Status
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                if (auto const ret = handler.process(); not ret)
 | 
			
		||||
                    return Error{ret.error()};  // forward Status
 | 
			
		||||
                else
 | 
			
		||||
                    return value_from(ret.value());
 | 
			
		||||
            }
 | 
			
		||||
                return value_from(ret.value());
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -88,7 +88,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(account), validation::Required{}, validation::AccountValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(account), validation::Required{}, validation::AccountValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -81,7 +81,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(account), validation::AccountValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -87,7 +87,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(account), validation::Required{}, validation::AccountValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(account), validation::Required{}, validation::AccountValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(account), validation::Required{}, validation::AccountValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(account), validation::Required{}, validation::AccountValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -81,7 +81,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(account), validation::Required{}, validation::AccountValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -60,7 +60,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(ledger_hash), validation::Uint256HexStringValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(taker_gets),
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const hotWalletValidator =
 | 
			
		||||
            validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(full), validation::Type<bool>{}, validation::NotSupported{true}},
 | 
			
		||||
 
 | 
			
		||||
@@ -76,7 +76,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static const auto rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(binary), validation::Type<bool>{}},
 | 
			
		||||
 
 | 
			
		||||
@@ -74,7 +74,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        // Validator only works in this handler
 | 
			
		||||
        // The accounts array must have two different elements
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@
 | 
			
		||||
namespace RPC {
 | 
			
		||||
 | 
			
		||||
LedgerRangeHandler::Result
 | 
			
		||||
LedgerRangeHandler::process() const
 | 
			
		||||
LedgerRangeHandler::process([[maybe_unused]] Context const& ctx) const
 | 
			
		||||
{
 | 
			
		||||
    // note: we can't get here if range is not available so it's safe
 | 
			
		||||
    return Output{sharedPtrBackend_->fetchLedgerRange().value()};
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Result
 | 
			
		||||
    process() const;
 | 
			
		||||
    process(Context const& ctx) const;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    friend void
 | 
			
		||||
 
 | 
			
		||||
@@ -81,7 +81,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(nft_id), validation::Required{}, validation::Uint256HexStringValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(nft_id), validation::Required{}, validation::Uint256HexStringValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(nft_id), validation::Required{}, validation::Uint256HexStringValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(account), validation::Required{}, validation::AccountValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ public:
 | 
			
		||||
    using Result = HandlerReturnType<Output>;
 | 
			
		||||
 | 
			
		||||
    Result
 | 
			
		||||
    process() const
 | 
			
		||||
    process([[maybe_unused]] Context const& ctx) const
 | 
			
		||||
    {
 | 
			
		||||
        return Output{};
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@
 | 
			
		||||
namespace RPC {
 | 
			
		||||
 | 
			
		||||
RandomHandler::Result
 | 
			
		||||
RandomHandler::process() const
 | 
			
		||||
RandomHandler::process([[maybe_unused]] Context const& ctx) const
 | 
			
		||||
{
 | 
			
		||||
    ripple::uint256 rand;
 | 
			
		||||
    beast::rngfill(rand.begin(), rand.size(), ripple::crypto_prng());
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,7 @@ public:
 | 
			
		||||
    using Result = HandlerReturnType<Output>;
 | 
			
		||||
 | 
			
		||||
    Result
 | 
			
		||||
    process() const;
 | 
			
		||||
    process(Context const& ctx) const;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    friend void
 | 
			
		||||
 
 | 
			
		||||
@@ -72,7 +72,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const booksValidator =
 | 
			
		||||
            validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const rpcSpec = RpcSpec{
 | 
			
		||||
            {JS(tx_hash), validation::Required{}, validation::Uint256HexStringValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static const RpcSpec rpcSpec = {
 | 
			
		||||
            {JS(transaction), validation::Required{}, validation::Uint256HexStringValidator},
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,7 @@ public:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    RpcSpecConstRef
 | 
			
		||||
    spec() const
 | 
			
		||||
    spec([[maybe_unused]] uint32_t apiVersion) const
 | 
			
		||||
    {
 | 
			
		||||
        static auto const booksValidator =
 | 
			
		||||
            validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
 | 
			
		||||
 
 | 
			
		||||
@@ -259,6 +259,9 @@ protected:
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    virtual ~Taggable() = default;
 | 
			
		||||
    Taggable(Taggable&&) = default;
 | 
			
		||||
    Taggable&
 | 
			
		||||
    operator=(Taggable&&) = default;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Getter for tag decorator.
 | 
			
		||||
 
 | 
			
		||||
@@ -32,37 +32,41 @@
 | 
			
		||||
 | 
			
		||||
namespace Web {
 | 
			
		||||
 | 
			
		||||
struct Context : public util::Taggable
 | 
			
		||||
struct Context : util::Taggable
 | 
			
		||||
{
 | 
			
		||||
    clio::Logger perfLog_{"Performance"};
 | 
			
		||||
    boost::asio::yield_context& yield;
 | 
			
		||||
    std::reference_wrapper<boost::asio::yield_context> yield;
 | 
			
		||||
    std::string method;
 | 
			
		||||
    std::uint32_t version;
 | 
			
		||||
    boost::json::object const& params;
 | 
			
		||||
    std::uint32_t apiVersion;
 | 
			
		||||
    boost::json::object params;
 | 
			
		||||
    std::shared_ptr<Server::ConnectionBase> session;
 | 
			
		||||
    Backend::LedgerRange const& range;
 | 
			
		||||
    Backend::LedgerRange range;
 | 
			
		||||
    std::string clientIp;
 | 
			
		||||
 | 
			
		||||
    Context(
 | 
			
		||||
        boost::asio::yield_context& yield_,
 | 
			
		||||
        std::string const& command_,
 | 
			
		||||
        std::uint32_t version_,
 | 
			
		||||
        boost::json::object const& params_,
 | 
			
		||||
        std::shared_ptr<Server::ConnectionBase> const& session_,
 | 
			
		||||
        util::TagDecoratorFactory const& tagFactory_,
 | 
			
		||||
        Backend::LedgerRange const& range_,
 | 
			
		||||
        std::string const& clientIp_)
 | 
			
		||||
        : Taggable(tagFactory_)
 | 
			
		||||
        , yield(yield_)
 | 
			
		||||
        , method(command_)
 | 
			
		||||
        , version(version_)
 | 
			
		||||
        , params(params_)
 | 
			
		||||
        , session(session_)
 | 
			
		||||
        , range(range_)
 | 
			
		||||
        , clientIp(clientIp_)
 | 
			
		||||
        boost::asio::yield_context& yield,
 | 
			
		||||
        std::string const& command,
 | 
			
		||||
        std::uint32_t apiVersion,
 | 
			
		||||
        boost::json::object params,
 | 
			
		||||
        std::shared_ptr<Server::ConnectionBase> const& session,
 | 
			
		||||
        util::TagDecoratorFactory const& tagFactory,
 | 
			
		||||
        Backend::LedgerRange const& range,
 | 
			
		||||
        std::string const& clientIp)
 | 
			
		||||
        : Taggable(tagFactory)
 | 
			
		||||
        , yield(std::ref(yield))
 | 
			
		||||
        , method(command)
 | 
			
		||||
        , apiVersion(apiVersion)
 | 
			
		||||
        , params(std::move(params))
 | 
			
		||||
        , session(session)
 | 
			
		||||
        , range(range)
 | 
			
		||||
        , clientIp(clientIp)
 | 
			
		||||
    {
 | 
			
		||||
        perfLog_.debug() << tag() << "new Context created";
 | 
			
		||||
        static clio::Logger perfLog{"Performance"};
 | 
			
		||||
        perfLog.debug() << tag() << "new Context created";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Context(Context&&) = default;
 | 
			
		||||
    Context&
 | 
			
		||||
    operator=(Context&&) = default;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace Web
 | 
			
		||||
 
 | 
			
		||||
@@ -19,8 +19,10 @@
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <rpc/Errors.h>
 | 
			
		||||
#include <rpc/Factories.h>
 | 
			
		||||
#include <rpc/RPCHelpers.h>
 | 
			
		||||
#include <rpc/common/impl/APIVersionParser.h>
 | 
			
		||||
#include <util/Profiler.h>
 | 
			
		||||
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
@@ -37,6 +39,7 @@ class RPCExecutor
 | 
			
		||||
    // subscription manager holds the shared_ptr of this class
 | 
			
		||||
    std::weak_ptr<SubscriptionManager> const subscriptions_;
 | 
			
		||||
    util::TagDecoratorFactory const tagFactory_;
 | 
			
		||||
    RPC::detail::ProductionAPIVersionParser apiVersionParser_;  // can be injected if needed
 | 
			
		||||
 | 
			
		||||
    clio::Logger log_{"RPC"};
 | 
			
		||||
    clio::Logger perfLog_{"Performance"};
 | 
			
		||||
@@ -48,7 +51,12 @@ public:
 | 
			
		||||
        std::shared_ptr<Engine> const& rpcEngine,
 | 
			
		||||
        std::shared_ptr<ETL const> const& etl,
 | 
			
		||||
        std::shared_ptr<SubscriptionManager> const& subscriptions)
 | 
			
		||||
        : backend_(backend), rpcEngine_(rpcEngine), etl_(etl), subscriptions_(subscriptions), tagFactory_(config)
 | 
			
		||||
        : backend_(backend)
 | 
			
		||||
        , rpcEngine_(rpcEngine)
 | 
			
		||||
        , etl_(etl)
 | 
			
		||||
        , subscriptions_(subscriptions)
 | 
			
		||||
        , tagFactory_(config)
 | 
			
		||||
        , apiVersionParser_(config.sectionOr("api_version", {}))
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -136,29 +144,39 @@ private:
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            auto const range = backend_->fetchLedgerRange();
 | 
			
		||||
            // for the error happened before the handler, we don't attach the clio warning
 | 
			
		||||
            if (!range)
 | 
			
		||||
            {
 | 
			
		||||
                // for error that happened before the handler, we don't attach any warnings
 | 
			
		||||
                rpcEngine_->notifyNotReady();
 | 
			
		||||
                return connection->send(
 | 
			
		||||
                    boost::json::serialize(composeError(RPC::RippledError::rpcNOT_READY)),
 | 
			
		||||
                    boost::beast::http::status::ok);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            auto context = connection->upgraded
 | 
			
		||||
                ? RPC::make_WsContext(
 | 
			
		||||
                      yc, request, connection, tagFactory_.with(connection->tag()), *range, connection->clientIp)
 | 
			
		||||
                : RPC::make_HttpContext(yc, request, tagFactory_.with(connection->tag()), *range, connection->clientIp);
 | 
			
		||||
            auto context = connection->upgraded ? RPC::make_WsContext(
 | 
			
		||||
                                                      yc,
 | 
			
		||||
                                                      request,
 | 
			
		||||
                                                      connection,
 | 
			
		||||
                                                      tagFactory_.with(connection->tag()),
 | 
			
		||||
                                                      *range,
 | 
			
		||||
                                                      connection->clientIp,
 | 
			
		||||
                                                      std::cref(apiVersionParser_))
 | 
			
		||||
                                                : RPC::make_HttpContext(
 | 
			
		||||
                                                      yc,
 | 
			
		||||
                                                      request,
 | 
			
		||||
                                                      tagFactory_.with(connection->tag()),
 | 
			
		||||
                                                      *range,
 | 
			
		||||
                                                      connection->clientIp,
 | 
			
		||||
                                                      std::cref(apiVersionParser_));
 | 
			
		||||
 | 
			
		||||
            if (!context)
 | 
			
		||||
            {
 | 
			
		||||
                perfLog_.warn() << connection->tag() << "Could not create RPC context";
 | 
			
		||||
                log_.warn() << connection->tag() << "Could not create RPC context";
 | 
			
		||||
                auto const err = context.error();
 | 
			
		||||
                perfLog_.warn() << connection->tag() << "Could not create Web context: " << err;
 | 
			
		||||
                log_.warn() << connection->tag() << "Could not create Web context: " << err;
 | 
			
		||||
 | 
			
		||||
                rpcEngine_->notifyBadSyntax();
 | 
			
		||||
                return connection->send(
 | 
			
		||||
                    boost::json::serialize(composeError(RPC::RippledError::rpcBAD_SYNTAX)),
 | 
			
		||||
                    boost::beast::http::status::ok);
 | 
			
		||||
                return connection->send(boost::json::serialize(composeError(err)), boost::beast::http::status::ok);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            auto [v, timeDiff] = util::timed([&]() { return rpcEngine_->buildResponse(*context); });
 | 
			
		||||
 
 | 
			
		||||
@@ -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");
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										137
									
								
								unittests/rpc/APIVersionTests.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								unittests/rpc/APIVersionTests.cpp
									
									
									
									
									
										Normal 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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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"({})"));
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user