From 1cb6f29b4076cb6502390886f24261264bf25239 Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Thu, 17 Sep 2026 16:33:22 +0100 Subject: [PATCH] refactor: Migrate handlers to rpc-spec (G) (#3213) --- src/rpc/CMakeLists.txt | 3 - src/rpc/README.md | 67 +- src/rpc/RPCHelpers.cpp | 12 +- src/rpc/RPCHelpers.hpp | 8 +- src/rpc/common/Checkers.hpp | 120 --- src/rpc/common/Concepts.hpp | 60 +- src/rpc/common/MetaProcessors.cpp | 54 -- src/rpc/common/MetaProcessors.hpp | 224 ----- src/rpc/common/Modifiers.hpp | 161 ---- src/rpc/common/Specs.cpp | 63 -- src/rpc/common/Specs.hpp | 132 --- src/rpc/common/ValidationHelpers.hpp | 117 --- src/rpc/common/Validators.cpp | 312 ------- src/rpc/common/Validators.hpp | 615 ------------- src/rpc/common/impl/Factories.hpp | 86 -- src/rpc/common/impl/Processors.hpp | 30 +- src/rpc/handlers/LedgerEntry.cpp | 14 +- src/rpc/handlers/VaultInfo.cpp | 11 +- tests/common/rpc/FakesAndMocks.hpp | 102 +-- tests/unit/CMakeLists.txt | 4 - tests/unit/rpc/BaseTests.cpp | 838 ------------------ tests/unit/rpc/ForwardingProxyTests.cpp | 11 + tests/unit/rpc/JsonBoolTests.cpp | 82 -- tests/unit/rpc/RPCEngineTests.cpp | 12 +- tests/unit/rpc/RPCHelpersTests.cpp | 46 +- tests/unit/rpc/common/CheckersTests.cpp | 78 -- tests/unit/rpc/common/SpecsTests.cpp | 284 ------ .../handlers/AccountMPTokenIssuancesTests.cpp | 8 +- .../rpc/handlers/DefaultProcessorTests.cpp | 65 +- tests/unit/rpc/handlers/LedgerEntryTests.cpp | 10 +- tests/unit/rpc/handlers/NFTInfoTests.cpp | 47 + tests/unit/rpc/handlers/TestHandlerTests.cpp | 7 +- tests/unit/rpc/handlers/VaultInfoTests.cpp | 24 +- 33 files changed, 221 insertions(+), 3486 deletions(-) delete mode 100644 src/rpc/common/Checkers.hpp delete mode 100644 src/rpc/common/MetaProcessors.cpp delete mode 100644 src/rpc/common/MetaProcessors.hpp delete mode 100644 src/rpc/common/Modifiers.hpp delete mode 100644 src/rpc/common/Specs.cpp delete mode 100644 src/rpc/common/Specs.hpp delete mode 100644 src/rpc/common/ValidationHelpers.hpp delete mode 100644 src/rpc/common/Validators.cpp delete mode 100644 src/rpc/common/Validators.hpp delete mode 100644 src/rpc/common/impl/Factories.hpp delete mode 100644 tests/unit/rpc/BaseTests.cpp delete mode 100644 tests/unit/rpc/JsonBoolTests.cpp delete mode 100644 tests/unit/rpc/common/CheckersTests.cpp delete mode 100644 tests/unit/rpc/common/SpecsTests.cpp diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt index 28cd5e098..2daf99925 100644 --- a/src/rpc/CMakeLists.txt +++ b/src/rpc/CMakeLists.txt @@ -16,9 +16,6 @@ target_sources( CredentialHelpers.cpp Counters.cpp WorkQueue.cpp - common/Specs.cpp - common/Validators.cpp - common/MetaProcessors.cpp common/impl/APIVersionParser.cpp common/impl/HandlerProvider.cpp common/impl/HandlerRegistry.cpp diff --git a/src/rpc/README.md b/src/rpc/README.md index 717a173e4..424b9daa6 100644 --- a/src/rpc/README.md +++ b/src/rpc/README.md @@ -4,27 +4,74 @@ The RPC subsystem is where the common framework for handling incoming JSON requests is implemented. +Request validation and deserialisation are **not** defined here. They live in the shared +[`xrpl-rpc-spec`](https://github.com/XRPLF/rpc-spec) library, consumed as ``, so that +one spec per RPC method can be shared verbatim between Clio and xrpld. Clio's job is to supply the +handler body and the response shape. + ## Components See the [common](https://github.com/XRPLF/clio/blob/develop/src/rpc/common) subfolder. - **AnyHandler**: The type-erased wrapper that allows for storing different handlers in one map/vector. -- **RpcSpec/FieldSpec**: The RPC specification classes, used to specify how incoming JSON is to be validated before it's parsed and passed on to individual handler implementations. -- **Validators/Modifiers**: A bunch of supported validators and modifiers that can be specified as requirements for each `FieldSpec` to make up the final `RpcSpec` of any given RPC handler. +- **Concepts**: The `SomeHandler` concept and its parts, which define what a handler must provide. +- **HandlerRegistry**: The table mapping a method name to its handler factory, plus whether the + method is Clio-only (and therefore never forwarded to xrpld). + +From the spec library: + +- **`rpc::spec::HandlerFor`**: Base class supplying the static `parseInput` and `spec` + entry points a handler needs. It resolves the versioned spec for `Input` through ADL, so a + handler names only its `Input` type. +- **`rpcspec/handlers//Types.hpp`**: The strongly-typed `Input` struct for a method + (`xrpl::AccountID`, `xrpl::uint256`, `LedgerSpecifier`, ... rather than `std::string`). +- **`rpcspec/handlers//Spec.hpp`**: The consteval spec declaring that method's fields, + their validators and their converters. ## Implementing a handler -See [tests/unit/rpc](https://github.com/XRPLF/clio/tree/develop/tests/unit/rpc) for examples. +See the existing handlers in [src/rpc/handlers](https://github.com/XRPLF/clio/tree/develop/src/rpc/handlers) +for examples; `NFTInfo` is a small one. -Handlers need to fulfil the requirements specified by the `SomeHandler` concept (see `rpc/common/Concepts.hpp`): +Handlers need to fulfil the requirements specified by the `SomeHandler` concept (see +`rpc/common/Concepts.hpp`): -- Expose types: - - `Input` - The POD struct which acts as input for the handler +- Derive from `rpc::spec::HandlerFor::Input>`. This supplies: - - `Output` - The POD struct which acts as output of a valid handler invocation + - `Input` — the strongly-typed input struct, owned by the spec library rather than declared here -- Have a `spec(uint32_t)` member function returning a const reference to an `RpcSpec` describing the JSON input for the specified API version. + - `static parseInput(boost::json::value const&, uint32_t apiVersion)` — validates and + deserialises in one pass, returning `std::expected` -- Have a `process(Input)` member function that operates on `Input` POD and returns `HandlerReturnType` + - `static spec(uint32_t apiVersion)` — returns a type-erased `rpc::spec::RpcSpecView` -- Implement `value_from` and `value_to` support using `tag_invoke` as per `boost::json` documentation for these functions. + If the method takes no input at all, skip the base class and expose only `process(Context const&)`. + +- Expose an `Output` POD struct which acts as output of a valid handler invocation. + +- Have a `process(Input const&, Context const&)` member function returning + `HandlerReturnType`. Cross-field checks that cannot be expressed in the spec belong here. + +- Implement `value_from` support for `Output` using `tag_invoke` as per `boost::json` + documentation. A `value_to` for `Input` is **not** needed — the spec deserialises it. + +- Register the method in `rpc/common/impl/HandlerRegistry.cpp`. + +If the method has no spec yet, add `Types.hpp` and `Spec.hpp` for it in the spec library first. + +> [!IMPORTANT] +> Do not hand-write `template struct rpc::spec::HandlerFor<...>;`, and do not include +> `` or `` from a handler. The explicit +> instantiations are generated by `rpcspec_generate_instantiations()` in `src/rpc/CMakeLists.txt`, +> one translation unit per method that has a `Spec.hpp`. A useful side effect is that every spec is +> compiled by Clio's build, so spec-side mistakes surface here. + +## Error messages + +A method's error codes and messages are part of its wire contract: clients parse them, so an +altered `error`, `error_code` or `error_message` is a breaking change. If a test disagrees with the +code, fix the spec rather than the expectation. + +Clio and xrpld do not always word the same failure identically. The spec library expresses those +differences per server rather than unifying early — `ifServerClio()` / `ifServerXrpld()` for +whole validators, and the `RPCSPEC_IS_CLIO` / `RPCSPEC_IS_XRPLD` macros for anything finer. diff --git a/src/rpc/RPCHelpers.cpp b/src/rpc/RPCHelpers.cpp index 0315d3ad7..ebd1cc32d 100644 --- a/src/rpc/RPCHelpers.cpp +++ b/src/rpc/RPCHelpers.cpp @@ -569,12 +569,12 @@ getLedgerHeaderFromLedgerSpecifier( return *maybeLgrInfo; } - if (resolved.isShortcut()) { - auto const shortcut = std::get(resolved.value); - ASSERT( - shortcut == rpc::spec::LedgerShortcut::Validated, - "current/closed ledgers must be forwarded before dispatch" - ); + // `current` and `closed` name ledgers Clio does not hold. Forwarding diverts them for the + // methods xrpld can answer; Clio-only methods are not forwarded, so they reach here. + if (resolved.isShortcut() and + std::get(resolved.value) != + rpc::spec::LedgerShortcut::Validated) { + return std::unexpected{Status{RippledError::RpcInvalidParams, "ledgerIndexMalformed"}}; } auto const ledgerSequence = resolved.isSequence() ? std::get(resolved.value) : maxSeq; diff --git a/src/rpc/RPCHelpers.hpp b/src/rpc/RPCHelpers.hpp index 2b2c338bd..d9a61c971 100644 --- a/src/rpc/RPCHelpers.hpp +++ b/src/rpc/RPCHelpers.hpp @@ -313,8 +313,12 @@ getLedgerHeaderFromHashOrSeq( * only serves validated data. An unspecified ledger resolves via * @c LedgerSpecifier::resolved(), which the spec library fixes to @c validated for Clio. * - * @c current and @c closed cannot reach here: @ref specifiesCurrentOrClosedLedger forwards - * those upstream before dispatch. + * @c current and @c closed name ledgers Clio does not hold, and yield @c invalidParams with + * @c ledgerIndexMalformed. + * @ref specifiesCurrentOrClosedLedger diverts such requests to xrpld before dispatch, but only + * for methods xrpld can answer: @c ForwardingProxy::shouldForward returns early for Clio-only + * methods, so those arrive here with the shortcut intact. The spec does not reject the two + * shortcuts either, since xrpld needs them. * * @param backend The backend to use * @param yield The coroutine context diff --git a/src/rpc/common/Checkers.hpp b/src/rpc/common/Checkers.hpp deleted file mode 100644 index 05ca7e103..000000000 --- a/src/rpc/common/Checkers.hpp +++ /dev/null @@ -1,120 +0,0 @@ -#pragma once - -#include "rpc/Errors.hpp" -#include "rpc/common/ValidationHelpers.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace rpc::check { -/** - * @brief Warning that checks can return - */ -struct Warning { - /** - * @brief Construct a new Warning object - * - * @param code The warning code - * @param message The warning message - */ - Warning(WarningCode code, std::string message) - : warningCode(code), extraMessage(std::move(message)) - { - } - - bool - operator==(Warning const& other) const = default; - - WarningCode warningCode; - std::string extraMessage; -}; -using Warnings = std::vector; - -/** - * @brief Check for a deprecated fields - */ -template -class Deprecated; - -/** - * @brief Check if a field is deprecated - */ -template <> -class Deprecated<> final { -public: - /** - * @brief Check if a field is deprecated - * - * @param value The json value to check - * @param key The key to check - * @return A warning if the field is deprecated or std::nullopt otherwise - */ - [[nodiscard]] static std::optional - check(boost::json::value const& value, std::string_view key) - { - if (value.is_object() and value.as_object().contains(key)) { - return Warning{ - WarningCode::WarnRpcDeprecated, fmt::format("Field '{}' is deprecated.", key) - }; - } - return std::nullopt; - } -}; - -/** - * @brief Check if a value of a field is deprecated - * @tparam T The type of the field - */ -template -class Deprecated final { - T value_; - -public: - /** - * @brief Construct a new Deprecated object - * - * @param val The value that is deprecated - */ - Deprecated(T val) : value_(std::move(val)) - { - } - - /** - * @brief Check if a value of a field is deprecated - * - * @param value The json value to check - * @param key The key to check - * @return A warning if the field is deprecated or std::nullopt otherwise - */ - [[nodiscard]] std::optional - check(boost::json::value const& value, std::string_view key) const - { - if (value.is_object() and value.as_object().contains(key) and - validation::checkType(value.as_object().at(key))) { - using boost::json::value_to; - auto const res = value_to(value.as_object().at(key)); - if (value_ == res) { - return Warning{ - WarningCode::WarnRpcDeprecated, - fmt::format("Value '{}' for field '{}' is deprecated", value_, key) - }; - } - } - return std::nullopt; - } -}; - -/** - * @brief Deduction guide for Deprecated - */ -template -Deprecated(T&&...) -> Deprecated; - -} // namespace rpc::check diff --git a/src/rpc/common/Concepts.hpp b/src/rpc/common/Concepts.hpp index df7be4d52..05dceaf30 100644 --- a/src/rpc/common/Concepts.hpp +++ b/src/rpc/common/Concepts.hpp @@ -1,7 +1,6 @@ #pragma once #include "rpc/Errors.hpp" -#include "rpc/common/Checkers.hpp" #include "rpc/common/Types.hpp" #include @@ -18,38 +17,6 @@ namespace rpc { -struct RpcSpec; - -/** - * @brief Specifies what a requirement used with @ref rpc::FieldSpec must provide. - */ -template -concept SomeRequirement = requires(T a, boost::json::value lval) { - { a.verify(lval, std::string{}) } -> std::same_as; -}; - -/** - * @brief Specifies what a modifier used with @ref rpc::FieldSpec must provide. - */ -template -concept SomeModifier = requires(T a, boost::json::value lval) { - { a.modify(lval, std::string{}) } -> std::same_as; -}; - -/** - * @brief Specifies what a check used with @ref rpc::FieldSpec must provide. - */ -template -concept SomeCheck = requires(T a, boost::json::value lval) { - { a.check(lval, std::string{}) } -> std::same_as>; -}; - -/** - * @brief The requirements of a processor to be used with @ref rpc::FieldSpec. - */ -template -concept SomeProcessor = (SomeRequirement or SomeModifier); - /** * @brief A process function that expects both some Input and a Context. */ @@ -67,25 +34,12 @@ concept SomeContextProcessWithoutInput = requires(T a, T::Output out, Context co { a.process(ctx) } -> std::same_as>; }; -/** - * @brief Specifies what a Handler with Input must provide. - */ -template -concept SomeHandlerWithInput = requires(T a, uint32_t version) { - { a.spec(version) } -> std::same_as; -} and SomeContextProcessWithInput and boost::json::has_value_to::value; - /** * @brief Specifies what a Handler validated by the shared consteval spec must provide. * * Such a handler inherits @c rpc::spec::HandlerFor from the spec library, which * supplies a static @c parseInput (validate and deserialise in one pass) and a static - * @c spec returning a type-erased @c RpcSpecView. Presence of @c parseInput is what - * selects this path over @c SomeHandlerWithInput. - * - * The two input paths are mutually exclusive by construction: a legacy handler returns - * @c RpcSpec @c const& from a non-static @c spec and needs a @c value_to for its Input, - * neither of which holds here. @c kIsSingleInputPath asserts that below. + * @c spec returning a type-erased @c RpcSpecView. */ template concept SomeHandlerWithTypedInput = requires(uint32_t version, boost::json::value jv) { @@ -100,21 +54,11 @@ concept SomeHandlerWithTypedInput = requires(uint32_t version, boost::json::valu template concept SomeHandlerWithoutInput = SomeContextProcessWithoutInput; -/** - * @brief True when @p T does not straddle the legacy and typed input paths. - * - * Guards the @c if @c constexpr chain in @c DefaultProcessor - were a handler to satisfy - * both, the dispatch order alone would silently decide which spec ran. - */ -template -constexpr bool kIsSingleInputPath = not(SomeHandlerWithInput and SomeHandlerWithTypedInput); - /** * @brief Specifies what a Handler type must provide. */ template -concept SomeHandler = - (SomeHandlerWithInput or SomeHandlerWithTypedInput or SomeHandlerWithoutInput) and +concept SomeHandler = (SomeHandlerWithTypedInput or SomeHandlerWithoutInput) and boost::json::has_value_from::value; } // namespace rpc diff --git a/src/rpc/common/MetaProcessors.cpp b/src/rpc/common/MetaProcessors.cpp deleted file mode 100644 index 6b6155d25..000000000 --- a/src/rpc/common/MetaProcessors.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include "rpc/common/MetaProcessors.hpp" - -#include "rpc/common/Types.hpp" - -#include -#include - -#include - -namespace rpc::meta { - -[[nodiscard]] MaybeError -Section::verify(boost::json::value& value, std::string_view key) const -{ - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - auto& res = value.as_object().at(key); - - // if it is not a json object, let other validators fail - if (!res.is_object()) - return {}; - - for (auto const& spec : specs_) { - if (auto const ret = spec.process(res); not ret) - return Error{ret.error()}; - } - - return {}; -} - -[[nodiscard]] MaybeError -ValidateArrayAt::verify(boost::json::value& value, std::string_view key) const -{ - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - if (not value.as_object().at(key).is_array()) - return Error{Status{RippledError::RpcInvalidParams}}; - - auto& arr = value.as_object().at(key).as_array(); - if (idx_ >= arr.size()) - return Error{Status{RippledError::RpcInvalidParams}}; - - auto& res = arr.at(idx_); - for (auto const& spec : specs_) { - if (auto const ret = spec.process(res); not ret) - return Error{ret.error()}; - } - - return {}; -} - -} // namespace rpc::meta diff --git a/src/rpc/common/MetaProcessors.hpp b/src/rpc/common/MetaProcessors.hpp deleted file mode 100644 index 09cf383a8..000000000 --- a/src/rpc/common/MetaProcessors.hpp +++ /dev/null @@ -1,224 +0,0 @@ -#pragma once - -#include "rpc/Errors.hpp" -#include "rpc/common/Concepts.hpp" -#include "rpc/common/Specs.hpp" -#include "rpc/common/Types.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace rpc::meta { - -/** - * @brief A meta-processor that acts as a spec for a sub-object/section. - */ -class Section final { - std::vector specs_; - -public: - /** - * @brief Construct new section validator from a list of specs. - * - * @param specs List of specs @ref FieldSpec - */ - explicit Section(std::initializer_list specs) : specs_{specs} - { - } - - /** - * @brief Verify that the JSON value representing the section is valid according to the given - * specs. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the section from the outer object - * @return Possibly an error - */ - [[nodiscard]] MaybeError - verify(boost::json::value& value, std::string_view key) const; -}; - -/** - * @brief A meta-processor that specifies a list of specs to run against the object at the given - * index in the array. - */ -class ValidateArrayAt final { - std::size_t idx_; - std::vector specs_; - -public: - /** - * @brief Constructs a processor that validates the specified element of a JSON array. - * - * @param idx The index inside the array to validate - * @param specs The specifications to validate against - */ - ValidateArrayAt(std::size_t idx, std::initializer_list specs) - : idx_{idx}, specs_{specs} - { - } - - /** - * @brief Verify that the JSON array element at given index is valid according the stored specs. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the array from the outer object - * @return Possibly an error - */ - [[nodiscard]] MaybeError - verify(boost::json::value& value, std::string_view key) const; -}; - -/** - * @brief A meta-processor that specifies a list of requirements to run against when the type - * matches the template parameter. - */ -template -class IfType final { -public: - /** - * @brief Constructs a validator that validates the specs if the type matches. - * @param requirements The requirements to validate against - */ - template - explicit IfType(Requirements&&... requirements) - : processor_( - [... r = std::forward( - requirements - )](boost::json::value& j, std::string_view key) -> MaybeError { - std::optional firstFailure = std::nullopt; - - // the check logic is the same as fieldspec - ( - [&j, &key, &firstFailure, req = &r]() { - if (firstFailure) - return; - - if (auto const res = req->verify(j, key); not res) - firstFailure = res.error(); - }(), - ...); - - if (firstFailure) - return Error{*firstFailure}; - - return {}; - } - ) - { - } - - IfType(IfType const&) = default; - IfType(IfType&&) = default; - - /** - * @brief Verify that the element is valid according to the stored requirements when type - * matches. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the element from the outer object - * @return Possibly an error - */ - [[nodiscard]] MaybeError - verify(boost::json::value& value, std::string_view key) const - { - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - if (not rpc::validation::checkType(value.as_object().at(key))) - return {}; // ignore if type does not match - - return processor_(value, key); - } - -private: - std::function processor_; -}; - -/** - * @brief A meta-processor that wraps a validator and produces a custom error in case the wrapped - * validator fails. - */ -template - requires SomeRequirement or SomeModifier -class WithCustomError final { - RequirementOrModifierType reqOrModifier_; - Status error_; - -public: - /** - * @brief Constructs a validator that calls the given validator `req` and returns a custom error - * `err` in case `req` fails. - * - * @param reqOrModifier The requirement to validate against - * @param err The custom error to return in case `req` fails - */ - WithCustomError(RequirementOrModifierType reqOrModifier, Status err) - : reqOrModifier_{std::move(reqOrModifier)}, error_{std::move(err)} - { - } - - /** - * @brief Runs the stored validator and produces a custom error if the wrapped validator fails. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the element from the outer object - * @return Possibly an error - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const - requires SomeRequirement - { - if (auto const res = reqOrModifier_.verify(value, key); not res) - return Error{error_}; - - return {}; - } - - /** - * @brief Runs the stored validator and produces a custom error if the wrapped validator fails. - * This is an overload for the requirement which can modify the value. Such as IfType. - * - * @param value The JSON value representing the outer object, this value can be modified by the - * requirement inside - * @param key The key used to retrieve the element from the outer object - * @return Possibly an error - */ - [[nodiscard]] MaybeError - verify(boost::json::value& value, std::string_view key) const - requires SomeRequirement - { - if (auto const res = reqOrModifier_.verify(value, key); not res) - return Error{error_}; - - return {}; - } - - /** - * @brief Runs the stored modifier and produces a custom error if the wrapped modifier fails. - * - * @param value The JSON value representing the outer object. This value can be modified by the - * modifier. - * @param key The key used to retrieve the element from the outer object - * @return Possibly an error - */ - MaybeError - modify(boost::json::value& value, std::string_view key) const - requires SomeModifier - - { - if (auto const res = reqOrModifier_.modify(value, key); not res) - return Error{error_}; - return {}; - } -}; - -} // namespace rpc::meta diff --git a/src/rpc/common/Modifiers.hpp b/src/rpc/common/Modifiers.hpp deleted file mode 100644 index 0b621220b..000000000 --- a/src/rpc/common/Modifiers.hpp +++ /dev/null @@ -1,161 +0,0 @@ -#pragma once - -#include "rpc/Errors.hpp" -#include "rpc/common/Types.hpp" -#include "util/JsonUtils.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace rpc::modifiers { - -/** - * @brief Clamp value between min and max. - */ -template -class Clamp final { - Type min_; - Type max_; - -public: - /** - * @brief Construct the modifier storing min and max values. - * - * @param min - * @param max - */ - explicit Clamp(Type min, Type max) : min_{min}, max_{max} - { - } - - /** - * @brief Clamp the value to stored min and max values. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the modified value from the outer object - * @return Possibly an error - */ - [[nodiscard]] MaybeError - modify(boost::json::value& value, std::string_view key) const - { - using boost::json::value_to; - - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - // clamp to min_ and max_ - auto const oldValue = value_to(value.as_object().at(key)); - value.as_object()[key] = std::clamp(oldValue, min_, max_); - - return {}; - } -}; - -/** - * @brief Convert input string to lower case. - * - * Note: the conversion is only performed if the input value is a string. - */ -struct ToLower final { - /** - * @brief Update the input string to lower case. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the modified value from the outer object - * @return Possibly an error - */ - [[nodiscard]] static MaybeError - modify(boost::json::value& value, std::string_view key) - { - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - if (not value.as_object().at(key).is_string()) - return {}; // ignore for non-string types - - value.as_object()[key] = - util::toLower(boost::json::value_to(value.as_object().at(key))); - return {}; - } -}; - -/** - * @brief Convert input string to integer. - * - * Note: the conversion is only performed if the input value is a string. - */ -struct ToNumber final { - /** - * @brief Update the input string to integer if it can be converted to integer by stoi. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the modified value from the outer object - * @return Possibly an error - */ - [[nodiscard]] static MaybeError - modify(boost::json::value& value, std::string_view key) - { - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - if (not value.as_object().at(key).is_string()) - return {}; // ignore for non-string types - - auto const strInt = boost::json::value_to(value.as_object().at(key)); - if (strInt.contains('.')) - return Error{Status{RippledError::RpcInvalidParams}}; // maybe a float - - try { - value.as_object()[key] = std::stoi(strInt); - } catch (std::exception& e) { - return Error{Status{RippledError::RpcInvalidParams}}; - } - return {}; - } -}; - -/** - * @brief Customised modifier allowing user define how to modify input in provided callable. - */ -class CustomModifier final { - std::function modifier_; - -public: - /** - * @brief Constructs a custom modifier from any supported callable. - * - * @tparam Fn The type of callable - * @param fn The callable/function object - */ - template - requires std::invocable - explicit CustomModifier(Fn&& fn) : modifier_{std::forward(fn)} - { - } - - /** - * @brief Modify the JSON value according to the custom modifier function stored. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return Any compatible user-provided error if modify/verify failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - modify(boost::json::value& value, std::string_view key) const - { - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - return modifier_(value.as_object().at(key), key); - }; -}; - -} // namespace rpc::modifiers diff --git a/src/rpc/common/Specs.cpp b/src/rpc/common/Specs.cpp deleted file mode 100644 index ab0919265..000000000 --- a/src/rpc/common/Specs.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include "rpc/common/Specs.hpp" - -#include "rpc/common/Checkers.hpp" -#include "rpc/common/Types.hpp" - -#include -#include -#include - -#include -#include -#include -#include - -namespace rpc { - -[[nodiscard]] MaybeError -FieldSpec::process(boost::json::value& value) const -{ - return processor_(value); -} - -[[nodiscard]] check::Warnings -FieldSpec::check(boost::json::value const& value) const -{ - return checker_(value); -} - -[[nodiscard]] MaybeError -RpcSpec::process(boost::json::value& value) const -{ - for (auto const& field : fields_) { - if (auto ret = field.process(value); not ret) - return Error{ret.error()}; - } - - return {}; -} - -[[nodiscard]] boost::json::array -RpcSpec::check(boost::json::value const& value) const -{ - std::unordered_map> warnings; - for (auto const& field : fields_) { - auto fieldWarnings = field.check(value); - for (auto& fw : fieldWarnings) { - warnings[fw.warningCode].push_back(std::move(fw.extraMessage)); - } - } - - boost::json::array result; - for (auto const& [code, messages] : warnings) { - auto warningObject = makeWarning(code); - auto& warningMessage = warningObject["message"].as_string(); - for (auto const& message : messages) { - warningMessage.append(" ").append(message); - } - result.push_back(std::move(warningObject)); - } - return result; -} - -} // namespace rpc diff --git a/src/rpc/common/Specs.hpp b/src/rpc/common/Specs.hpp deleted file mode 100644 index 5a5e6eaa7..000000000 --- a/src/rpc/common/Specs.hpp +++ /dev/null @@ -1,132 +0,0 @@ -#pragma once - -#include "rpc/common/Checkers.hpp" -#include "rpc/common/Concepts.hpp" -#include "rpc/common/Types.hpp" -#include "rpc/common/impl/Factories.hpp" - -#include -#include - -#include -#include -#include -#include - -namespace rpc { - -/** - * @brief Represents a Specification for one field of an RPC command. - */ -struct FieldSpec final { - /** - * @brief Construct a field specification out of a set of processors. - * - * @tparam Processors The types of processors - * @param key The key in a JSON object that the field validates - * @param processors The processors, each of them have to fulfil the @ref rpc::SomeProcessor - * concept - */ - template - FieldSpec(std::string const& key, Processors&&... processors) - : processor_{ - impl::makeFieldProcessor(key, std::forward(processors)...) - } - , checker_{impl::kEmptyFieldChecker} - { - } - - /** - * @brief Construct a field specification out of a set of checkers. - * - * @tparam Checks The types of checkers - * @param key The key in a JSON object that the field validates - * @param checks The checks, each of them have to fulfil the @ref rpc::SomeCheck concept - */ - template - FieldSpec(std::string const& key, Checks&&... checks) - : processor_{impl::kEmptyFieldProcessor} - , checker_{impl::makeFieldChecker(key, std::forward(checks)...)} - { - } - - /** - * @brief Processes the passed JSON value using the stored processors. - * - * @param value The JSON value to validate and/or modify - * @return Nothing on success; Status on error - */ - [[nodiscard]] MaybeError - process(boost::json::value& value) const; - - /** - * @brief Checks the passed JSON value using the stored checkers. - * - * @param value The JSON value to validate - * @return A vector of warnings (empty if no warnings) - */ - [[nodiscard]] check::Warnings - check(boost::json::value const& value) const; - -private: - impl::FieldSpecProcessor processor_; - impl::FieldChecker checker_; -}; - -/** - * @brief Represents a Specification of an entire RPC command. - * - * Note: this should really be all constexpr and handlers would expose - * static constexpr RpcSpec spec instead. Maybe some day in the future. - */ -struct RpcSpec final { - /** - * @brief Construct a full RPC request specification. - * - * @param fields The fields of the RPC specification @ref FieldSpec - */ - RpcSpec(std::initializer_list fields) : fields_{fields} - { - } - - /** - * @brief Construct a full RPC request specification from another spec and additional fields. - * - * @param other The other spec to copy fields from - * @param additionalFields The additional fields to add to the spec - */ - RpcSpec(RpcSpec const& other, std::initializer_list additionalFields) - : fields_{other.fields_} - { - for (auto& f : additionalFields) - fields_.push_back(f); - } - - /** - * @brief Processes the passed JSON value using the stored field specs. - * - * @param value The JSON value to validate and/or modify - * @return Nothing on success; Status on error - */ - [[nodiscard]] MaybeError - process(boost::json::value& value) const; - - /** - * @brief Checks the passed JSON value using the stored field specs. - * - * @param value The JSON value to validate - * @return JSON array of warnings (empty if no warnings) - */ - [[nodiscard]] boost::json::array - check(boost::json::value const& value) const; - -private: - std::vector fields_; -}; - -/** - * @brief An alias for a const reference to @ref RpcSpec. - */ -using RpcSpecConstRef = RpcSpec const&; - -} // namespace rpc diff --git a/src/rpc/common/ValidationHelpers.hpp b/src/rpc/common/ValidationHelpers.hpp deleted file mode 100644 index 0c143b43e..000000000 --- a/src/rpc/common/ValidationHelpers.hpp +++ /dev/null @@ -1,117 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace rpc::validation { -namespace impl { - -template -void -clampAs(boost::json::value& value) -{ - if (value.is_uint64()) { - auto const valueUint = value.as_uint64(); - if (valueUint > static_cast(std::numeric_limits::max())) - value = std::numeric_limits::max(); - } else if (value.is_int64()) { - auto const valueInt = value.as_int64(); - if (valueInt > static_cast(std::numeric_limits::max())) - value = std::numeric_limits::max(); - } -} - -template -void -clampAs(boost::json::value& value) -{ - if (value.is_uint64()) { - auto const valueUint = value.as_uint64(); - if (valueUint > static_cast(std::numeric_limits::max())) - value = std::numeric_limits::max(); - } else if (value.is_int64()) { - auto const valueInt = value.as_int64(); - if (valueInt > static_cast(std::numeric_limits::max())) { - value = std::numeric_limits::max(); - } else if (valueInt < static_cast(std::numeric_limits::min())) { - value = std::numeric_limits::min(); - } - } -} - -} // namespace impl - -/** - * @brief Check that the type is the same as what was expected. - * - * @tparam Expected The expected type that value should be convertible to - * @param value The json value to check the type of - * @return true if convertible; false otherwise - */ -template -[[nodiscard]] bool -checkType(boost::json::value const& value) -{ - auto hasError = false; - if constexpr (std::is_same_v) { - if (not value.is_bool()) - hasError = true; - } else if constexpr (std::is_same_v) { - if (not value.is_string()) - hasError = true; - } else if constexpr (std::is_same_v or std::is_same_v) { - if (not value.is_double()) - hasError = true; - } else if constexpr (std::is_same_v) { - if (not value.is_array()) - hasError = true; - } else if constexpr (std::is_same_v) { - if (not value.is_object()) - hasError = true; - } else if constexpr ( - std::is_convertible_v or std::is_convertible_v - ) { - if (not value.is_int64() && not value.is_uint64()) - hasError = true; - // if the type specified is unsigned, it should not be negative - if constexpr (std::is_unsigned_v) { - if (value.is_int64() and value.as_int64() < 0) - hasError = true; - } - } - - return not hasError; -} - -/** - * @brief Check that the type is the same as what was expected optionally clamping it into range. - * - * This is used to automatically clamp the value into the range available to the specified type. It - * is needed in order to avoid Min, Max and other validators throw "not exact" error from Boost.Json - * library if the value does not fit in the specified type. - * - * @tparam Expected The expected type that value should be convertible to - * @param value The json value to check the type of - * @return true if convertible; false otherwise - */ -template -[[nodiscard]] bool -checkTypeAndClamp(boost::json::value& value) -{ - if (not checkType(value)) - return false; // fails basic type check - - if constexpr (std::is_integral_v and not std::is_same_v) - impl::clampAs(value); - - return true; -} - -} // namespace rpc::validation diff --git a/src/rpc/common/Validators.cpp b/src/rpc/common/Validators.cpp deleted file mode 100644 index 506f4e903..000000000 --- a/src/rpc/common/Validators.cpp +++ /dev/null @@ -1,312 +0,0 @@ -#include "rpc/common/Validators.hpp" - -#include "rpc/JS.hpp" -#include "rpc/RPCHelpers.hpp" -#include "rpc/common/Types.hpp" -#include "util/AccountUtils.hpp" -#include "util/TimeUtils.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace rpc::validation { - -[[nodiscard]] MaybeError -Required::verify(boost::json::value const& value, std::string_view key) -{ - if (not value.is_object() or not value.as_object().contains(key)) { - return Error{Status{ - RippledError::RpcInvalidParams, "Required field '" + std::string{key} + "' missing" - }}; - } - - return {}; -} - -[[nodiscard]] MaybeError -TimeFormatValidator::verify(boost::json::value const& value, std::string_view key) const -{ - using boost::json::value_to; - - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - if (not value.as_object().at(key).is_string()) - return Error{Status{RippledError::RpcInvalidParams}}; - - auto const ret = - util::systemTpFromUtcStr(value_to(value.as_object().at(key)), format_); - if (!ret) - return Error{Status{RippledError::RpcInvalidParams}}; - - return {}; -} - -[[nodiscard]] MaybeError -CustomValidator::verify(boost::json::value const& value, std::string_view key) const -{ - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - return validator_(value.as_object().at(key), key); -} - -[[nodiscard]] bool -checkIsU32Numeric(std::string_view sv) -{ - uint32_t unused = 0; - auto [_, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), unused); - - return ec == std::errc(); -} - -CustomValidator CustomValidators::uint160HexStringValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - return makeHexStringValidator(value, key); - }}; - -CustomValidator CustomValidators::uint192HexStringValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - return makeHexStringValidator(value, key); - }}; - -CustomValidator CustomValidators::uint256HexStringValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - return makeHexStringValidator(value, key); - }}; - -CustomValidator CustomValidators::ledgerIndexValidator = - CustomValidator{[](boost::json::value const& value, std::string_view /* key */) -> MaybeError { - auto err = Error{Status{RippledError::RpcInvalidParams, "ledgerIndexMalformed"}}; - - if (!value.is_string() && !(value.is_uint64() || value.is_int64())) - return err; - - if (value.is_string() && value.as_string() != "validated" && - !checkIsU32Numeric(boost::json::value_to(value))) - return err; - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::accountBase58Validator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_string()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"}}; - - auto const account = - util::parseBase58Wrapper(boost::json::value_to(value)); - if (!account || account->isZero()) - return Error{Status{ClioError::RpcMalformedAddress}}; - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::accountMarkerValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_string()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"}}; - - // TODO: we are using parseAccountCursor from RPCHelpers, after we - // remove all old handler, this function can be moved to here - if (!parseAccountCursor(boost::json::value_to(value))) { - // align with the current error message - return Error{Status{RippledError::RpcInvalidParams, "Malformed cursor."}}; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::accountTypeValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_string()) { - return Error{Status{ - RippledError::RpcInvalidParams, fmt::format("Invalid field '{}', not string.", key) - }}; - } - - auto const type = - rpc::spec::accountOwnedLedgerTypeFromStr(boost::json::value_to(value)); - if (type == xrpl::ltANY) { - return Error{ - Status{RippledError::RpcInvalidParams, fmt::format("Invalid field '{}'.", key)} - }; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::currencyValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_string()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"}}; - - auto const currencyStr = boost::json::value_to(value); - if (currencyStr.empty()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "IsEmpty"}}; - - xrpl::Currency currency; - if (!xrpl::toCurrency(currency, currencyStr)) - return Error{Status{ClioError::RpcMalformedCurrency, "malformedCurrency"}}; - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::bookTakerValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (!value.is_object()) - return {}; // type is validated separately by Type - - auto const& obj = value.as_object(); - bool const hasCurrency = obj.contains("currency"); - bool const hasMptId = obj.contains("mpt_issuance_id"); - - if (!hasCurrency && !hasMptId) { - return Error{Status{ - RippledError::RpcInvalidParams, fmt::format("Missing field '{}.currency'.", key) - }}; - } - - if (hasMptId && (hasCurrency || obj.contains("issuer"))) { - return Error{ - Status{RippledError::RpcInvalidParams, fmt::format("Invalid field '{}'.", key)} - }; - } - - // Wrong type -> invalidParams (xrpld's validateTakerJSON), checked here before the - // per-field validators so they can own bad *values* -> dst/srcAmtMalformed. - if ((hasCurrency && !obj.at(JS(currency)).is_string()) || - (hasMptId && !obj.at(JS(mpt_issuance_id)).is_string())) { - return Error{Status{ - RippledError::RpcInvalidParams, - fmt::format("Invalid field '{}.currency', not string.", key) - }}; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::currencyIssueValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (not value.is_object()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotObject"}}; - - try { - parseIssue(value.as_object()); - } catch (std::runtime_error const&) { - return Error{Status{ClioError::RpcMalformedRequest}}; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::credentialTypeValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (not value.is_string()) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, std::string(key) + " NotString" - }}; - } - - auto const& credTypeHex = xrpl::strUnHex(value.as_string()); - if (!credTypeHex.has_value()) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, std::string(key) + " NotHexString" - }}; - } - - if (credTypeHex->empty()) { - return Error{ - Status{ClioError::RpcMalformedAuthorizedCredentials, std::string(key) + " is empty"} - }; - } - - if (credTypeHex->size() > xrpl::kMaxCredentialTypeLength) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, - std::string(key) + " greater than max length" - }}; - } - - return MaybeError{}; - }}; - -CustomValidator CustomValidators::authorizeCredentialValidator = - CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError { - if (not value.is_array()) - return Error{Status{ClioError::RpcMalformedRequest, std::string(key) + " not array"}}; - - auto const& authCred = value.as_array(); - if (authCred.empty()) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, - fmt::format("Requires at least one element in authorized_credentials array.") - }}; - } - - if (authCred.size() > xrpl::kMaxCredentialsArraySize) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, - fmt::format( - "Max {} number of credentials in authorized_credentials array", - xrpl::kMaxCredentialsArraySize - ) - }}; - } - - for (auto const& credObj : value.as_array()) { - if (!credObj.is_object()) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, - "authorized_credentials elements in array are not objects." - }}; - } - auto const& obj = credObj.as_object(); - - if (!obj.contains("issuer")) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, - "Field 'Issuer' is required but missing." - }}; - } - - // don't want to change issuer error message to be about credentials - if (!accountBase58Validator.verify(credObj, "issuer")) { - return Error{ - Status{ClioError::RpcMalformedAuthorizedCredentials, "issuer NotString"} - }; - } - - if (!obj.contains("credential_type")) { - return Error{Status{ - ClioError::RpcMalformedAuthorizedCredentials, - "Field 'CredentialType' is required but missing." - }}; - } - - if (auto const err = credentialTypeValidator.verify(credObj, "credential_type"); !err) - return err; - } - - return MaybeError{}; - }}; - -} // namespace rpc::validation diff --git a/src/rpc/common/Validators.hpp b/src/rpc/common/Validators.hpp deleted file mode 100644 index a5ae7c7d0..000000000 --- a/src/rpc/common/Validators.hpp +++ /dev/null @@ -1,615 +0,0 @@ -#pragma once - -#include "rpc/Errors.hpp" -#include "rpc/common/Types.hpp" -#include "rpc/common/ValidationHelpers.hpp" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace rpc::validation { - -/** - * @brief A validator that simply requires a field to be present. - */ -struct Required final { - /** - * @brief Verify that the JSON value is present and not null. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return An error if validation failed; otherwise no error is returned - */ - [[nodiscard]] static MaybeError - verify(boost::json::value const& value, std::string_view key); -}; - -/** - * @brief A validator that forbids a field to be present. - * - * If there is a value provided, it will forbid the field only when the value equals. - * If there is no value provided, it will forbid the field when the field shows up. - */ -template -class NotSupported; - -/** - * @brief A specialized NotSupported validator that forbids a field to be present when the value - * equals the given value. - */ -template -class NotSupported final { - T value_; - -public: - /** - * @brief Constructs a new NotSupported validator. - * - * @param val The value to store and verify against - */ - NotSupported(T val) : value_(val) - { - } - - /** - * @brief Verify whether the field is supported or not. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcNotSupported` if the value matched; otherwise no error is returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const - { - if (value.is_object() and value.as_object().contains(key)) { - using boost::json::value_to; - auto const res = value_to(value.as_object().at(key)); - if (value_ == res) { - return Error{Status{ - RippledError::RpcNotSupported, - fmt::format("Not supported field '{}'s value '{}'", std::string{key}, res) - }}; - } - } - return {}; - } -}; - -/** - * @brief A specialized NotSupported validator that forbids a field to be present. - */ -template <> -class NotSupported<> final { -public: - /** - * @brief Verify whether the field is supported or not. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcNotSupported` if the field is found; otherwise no error is - * returned - */ - [[nodiscard]] static MaybeError - verify(boost::json::value const& value, std::string_view key) - { - if (value.is_object() and value.as_object().contains(key)) { - return Error{Status{ - RippledError::RpcNotSupported, "Not supported field '" + std::string{key} + '\'' - }}; - } - - return {}; - } -}; - -/** - * @brief Deduction guide to avoid having to specify the template arguments. - */ -template -NotSupported(T&&... t) -> NotSupported; - -/** - * @brief Validates that the type of the value is one of the given types. - */ -template -struct Type final { - /** - * @brief Verify that the JSON value is (one) of specified type(s). - * @note The value itself can only change for integral types and only if the value is outside of - * the range of the expected integer type (see checkTypeAndClamp). - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value& value, std::string_view key) const - { - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. If field is supposed to exist, let 'required' fail instead - - auto& res = value.as_object().at(key); - auto const convertible = (checkTypeAndClamp(res) || ...); - - if (not convertible) - return Error{Status{RippledError::RpcInvalidParams}}; - - return {}; - } -}; - -/** - * @brief Validate that value is between specified min and max. - */ -template -class Between final { - Type min_; - Type max_; - -public: - /** - * @brief Construct the validator storing min and max values. - * - * @param min - * @param max - */ - explicit Between(Type min, Type max) : min_{min}, max_{max} - { - } - - /** - * @brief Verify that the JSON value is within a certain range. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const - { - using boost::json::value_to; - - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - auto const res = value_to(value.as_object().at(key)); - - // TODO: may want a way to make this code more generic (e.g. use a free - // function that can be overridden for this comparison) - if (res < min_ || res > max_) - return Error{Status{RippledError::RpcInvalidParams}}; - - return {}; - } -}; - -/** - * @brief Validate that value is equal or greater than the specified min. - */ -template -class Min final { - Type min_; - -public: - /** - * @brief Construct the validator storing min value. - * - * @param min - */ - explicit Min(Type min) : min_{min} - { - } - - /** - * @brief Verify that the JSON value is not smaller than min - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const - { - using boost::json::value_to; - - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - auto const res = value_to(value.as_object().at(key)); - - if (res < min_) - return Error{Status{RippledError::RpcInvalidParams}}; - - return {}; - } -}; - -/** - * @brief Validate that value is not greater than max. - */ -template -class Max final { - Type max_; - -public: - /** - * @brief Construct the validator storing max value. - * - * @param max - */ - explicit Max(Type max) : max_{max} - { - } - - /** - * @brief Verify that the JSON value is not greater than max. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const - { - using boost::json::value_to; - - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - auto const res = value_to(value.as_object().at(key)); - - if (res > max_) - return Error{Status{RippledError::RpcInvalidParams}}; - - return {}; - } -}; - -/** - * @brief Validate that value can be converted to time according to the given format. - */ -class TimeFormatValidator final { - std::string format_; - -public: - /** - * @brief Construct the validator storing format value. - * - * @param format The format to use for time conversion - */ - explicit TimeFormatValidator(std::string format) : format_{std::move(format)} - { - } - - /** - * @brief Verify that the JSON value is valid formatted time. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const; -}; - -/** - * @brief Validates that the value is equal to the one passed in. - */ -template -class EqualTo final { - Type original_; - -public: - /** - * @brief Construct the validator with stored original value. - * - * @param original The original value to store - */ - explicit EqualTo(Type original) : original_{std::move(original)} - { - } - - /** - * @brief Verify that the JSON value is equal to the stored original. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const - { - using boost::json::value_to; - - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - auto const res = value_to(value.as_object().at(key)); - if (res != original_) - return Error{Status{RippledError::RpcInvalidParams}}; - - return {}; - } -}; - -/** - * @brief Deduction guide to help disambiguate what it means to EqualTo a "string" without - * specifying the type. - */ -EqualTo(char const*) -> EqualTo; - -/** - * @brief Validates that the value is one of the values passed in. - */ -template -class OneOf final { - std::vector options_; - -public: - /** - * @brief Construct the validator with stored options of initializer list. - * - * @param options The list of allowed options - */ - explicit OneOf(std::initializer_list options) : options_{options} - { - } - - /** - * @brief Construct the validator with stored options of other container. - * - * @param begin,end the range to copy the elements from - */ - template - explicit OneOf(InputIt begin, InputIt end) : options_{begin, end} - { - } - - /** - * @brief Verify that the JSON value is one of the stored options. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const - { - using boost::json::value_to; - - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. field does not exist, let 'required' fail instead - - auto const res = value_to(value.as_object().at(key)); - if (std::find(std::begin(options_), std::end(options_), res) == std::end(options_)) { - return Error{ - Status{RippledError::RpcInvalidParams, fmt::format("Invalid field '{}'.", key)} - }; - } - - return {}; - } -}; - -/** - * @brief Deduction guide to help disambiguate what it means to OneOf a few "strings" without - * specifying the type. - */ -OneOf(std::initializer_list) -> OneOf; - -/** - * @brief A meta-validator that allows to specify a custom validation function. - */ -class CustomValidator final { - std::function validator_; - -public: - /** - * @brief Constructs a custom validator from any supported callable. - * - * @tparam Fn The type of callable - * @param fn The callable/function object - */ - template - requires std::invocable - explicit CustomValidator(Fn&& fn) : validator_{std::forward(fn)} - { - } - - /** - * @brief Verify that the JSON value is valid according to the custom validation function - * stored. - * - * @param value The JSON value representing the outer object - * @param key The key used to retrieve the tested value from the outer object - * @return Any compatible user-provided error if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] MaybeError - verify(boost::json::value const& value, std::string_view key) const; -}; - -/** - * @brief Helper function to check if input value is an uint32 number or not. - * - * @param sv The input value as a string_view - * @return true if the string can be converted to a uint32; false otherwise - */ -[[nodiscard]] bool -checkIsU32Numeric(std::string_view sv); - -template - requires( - std::is_same_v || std::is_same_v || - std::is_same_v - ) -MaybeError -makeHexStringValidator(boost::json::value const& value, std::string_view key) -{ - if (!value.is_string()) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"}}; - - HexType parsedInt; - if (!parsedInt.parseHex(value.as_string().c_str())) - return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "Malformed"}}; - - return MaybeError{}; -} - -/** - * @brief A group of custom validation functions - */ -struct CustomValidators final { - /** - * @brief Provides a commonly used validator for ledger index. - * - * LedgerIndex must be a string or an int. If the specified LedgerIndex is a string, its value - * must be either "validated" or a valid integer value represented as a string. - */ - static CustomValidator ledgerIndexValidator; - - /** - * @brief Provides a commonly used validator for accounts. - * - * Account must be a string and can convert to base58. - */ - static CustomValidator accountBase58Validator; - - /** - * @brief Provides a commonly used validator for markers. - * - * A marker is composed of a comma-separated index and a start hint. - * The former will be read as hex, and the latter can be cast to uint64. - */ - static CustomValidator accountMarkerValidator; - - /** - * @brief Provides a validator for account type. - * - * A type accepts canonical names of owned ledger entry types (case insensitive) or short names. - * Used by account_objects. - */ - static CustomValidator accountTypeValidator; - - /** - * @brief Provides a commonly used validator for uint160(AccountID) hex string. - * - * It must be a string and also a decodable hex. - * AccountID uses this validator. - */ - static CustomValidator uint160HexStringValidator; - - /** - * @brief Provides a commonly used validator for uint192 hex string. - * - * It must be a string and also a decodable hex. - * MPTIssuanceID uses this validator. - */ - static CustomValidator uint192HexStringValidator; - - /** - * @brief Provides a commonly used validator for uint256 hex string. - * - * It must be a string and also a decodable hex. - * Transaction index, ledger hash all use this validator. - */ - static CustomValidator uint256HexStringValidator; - - /** - * @brief Provides a commonly used validator for currency, including standard currency code and - * token code. - */ - static CustomValidator currencyValidator; - - /** - * @brief Validates an asset (xrpl::Issue). - * - * Used by amm_info. - */ - static CustomValidator currencyIssueValidator; - - /** - * @brief Validates a book taker object (`taker_gets`/`taker_pays`). - * - * The object must specify an asset as either a `currency` (optionally with an `issuer`) or an - * `mpt_issuance_id`, but not both, and `mpt_issuance_id` must not be combined with `issuer`. - * Mirrors `xrpld`'s `validateTakerJSON`: a missing asset yields `Missing field - * '.currency'.`, conflicting fields yield `Invalid field ''.`, and a present but - * non-string `currency`/`mpt_issuance_id` yields `Invalid field '.currency', not - * string.` (all `invalidParams`). The field name is taken from the validated key. - * - * Used by book_offers. - */ - static CustomValidator bookTakerValidator; - - /** - * @brief Provides a validator for validating authorized_credentials json array. - * - * Used by deposit_preauth. - */ - static CustomValidator authorizeCredentialValidator; - - /** - * @brief Provides a validator for validating credential_type. - * - * Used by AuthorizeCredentialValidator in deposit_preauth and by the credential - * object lookup in ledger_entry. - */ - static CustomValidator credentialTypeValidator; -}; - -/** - * @brief Validates that the elements of the array is of type Hex256 uint - */ -struct Hex256ItemType final { - /** - * @brief Validates given the prerequisite that the type of the json value is an array, - * verifies all values within the array is of uint256 hash - * - * @param value the value to verify - * @param key The key used to retrieve the tested value from the outer object - * @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is - * returned - */ - [[nodiscard]] static MaybeError - verify(boost::json::value const& value, std::string_view key) - { - if (not value.is_object() or not value.as_object().contains(key)) - return {}; // ignore. If field is supposed to exist, let 'required' fail instead - - auto const& res = value.as_object().at(key); - - // loop through each item in the array and make sure it is uint256 hex string - for (auto const& elem : res.as_array()) { - xrpl::uint256 num; - if (!elem.is_string() || !num.parseHex(elem.as_string())) { - return Error{ - Status{RippledError::RpcInvalidParams, "Item is not a valid uint256 type."} - }; - } - } - return {}; - } -}; - -} // namespace rpc::validation diff --git a/src/rpc/common/impl/Factories.hpp b/src/rpc/common/impl/Factories.hpp deleted file mode 100644 index 9980178fd..000000000 --- a/src/rpc/common/impl/Factories.hpp +++ /dev/null @@ -1,86 +0,0 @@ -#pragma once - -#include "rpc/Errors.hpp" -#include "rpc/common/Checkers.hpp" -#include "rpc/common/Concepts.hpp" -#include "rpc/common/Types.hpp" -#include "util/UnsupportedType.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace rpc::impl { - -using FieldSpecProcessor = std::function; - -static FieldSpecProcessor const kEmptyFieldProcessor = [](boost::json::value&) -> MaybeError { - return {}; -}; - -template -[[nodiscard]] FieldSpecProcessor -makeFieldProcessor(std::string const& key, Processors&&... procs) -{ - return [key, ... proc = std::forward(procs)](boost::json::value& j) -> MaybeError { - std::optional firstFailure = std::nullopt; - - // This expands in order of Requirements and stops evaluating after first failure which is - // stored in `firstFailure` and can be checked later on to see whether the verification - // failed as a whole or not. - ( - [&j, &key, &firstFailure, req = &proc]() { - if (firstFailure) - return; // already failed earlier - skip - - if constexpr (SomeRequirement) { - if (auto const res = req->verify(j, key); not res) - firstFailure = res.error(); - } else if constexpr (SomeModifier) { - if (auto const res = req->modify(j, key); not res) - firstFailure = res.error(); - } else { - static_assert(util::Unsupported); - } - }(), - ...); - - if (firstFailure) - return std::unexpected{std::move(firstFailure).value()}; - - return {}; - }; -} - -using FieldChecker = std::function; - -static FieldChecker const kEmptyFieldChecker = [](boost::json::value const&) -> check::Warnings { - return {}; -}; - -template -[[nodiscard]] FieldChecker -makeFieldChecker(std::string const& key, Checks&&... checks) -{ - return [key, - ... checks = - std::forward(checks)](boost::json::value const& j) -> check::Warnings { - check::Warnings warnings; - // This expands in order of Checks and collects all warnings into a WarningsCollection - ( - [&j, &key, &warnings, req = &checks]() { - if (auto res = req->check(j, key); res) - warnings.push_back(*std::move(res)); - }(), - ...); - return warnings; - }; -} - -} // namespace rpc::impl diff --git a/src/rpc/common/impl/Processors.hpp b/src/rpc/common/impl/Processors.hpp index 1604af1d2..e9595d136 100644 --- a/src/rpc/common/impl/Processors.hpp +++ b/src/rpc/common/impl/Processors.hpp @@ -20,20 +20,12 @@ struct DefaultProcessor final { ) const { using boost::json::value_from; - using boost::json::value_to; static_assert( - kIsSingleInputPath, - "handler satisfies both the legacy and the typed input path; dispatch would be " - "decided by the order of the branches below rather than by the handler" - ); - static_assert( - SomeHandlerWithTypedInput or SomeHandlerWithInput or - SomeHandlerWithoutInput, + SomeHandlerWithTypedInput or SomeHandlerWithoutInput, "handler matches none of the branches below" ); - // New `rpc-spec`-based handler if constexpr (SomeHandlerWithTypedInput) { auto input = HandlerType::parseInput(value, ctx.apiVersion); auto warnings = rpc::spec::toJsonArray(HandlerType::spec(ctx.apiVersion).check(value)); @@ -49,26 +41,6 @@ struct DefaultProcessor final { return ReturnType{value_from(std::move(ret).value()), std::move(warnings)}; } - if constexpr (SomeHandlerWithInput) { - // Old spec-based handler: first we run validation against specified API version - // TODO: This will be eventually removed once fully migraded to new rpc-spec system. - auto const spec = handler.spec(ctx.apiVersion); - auto warnings = spec.check(value); - auto input = value; // copy here, spec require mutable data - - if (auto const ret = spec.process(input); not ret.has_value()) - return ReturnType{Error{ret.error()}, std::move(warnings)}; // forward Status - - auto const inData = value_to(input); - auto ret = handler.process(inData, ctx); - - // real handler is given expected Input, not json - if (not ret.has_value()) - return ReturnType{Error{std::move(ret).error()}, std::move(warnings)}; - - return ReturnType{value_from(std::move(ret).value()), std::move(warnings)}; - } - if constexpr (SomeHandlerWithoutInput) { // no input to pass, ignore the value auto const ret = handler.process(ctx); diff --git a/src/rpc/handlers/LedgerEntry.cpp b/src/rpc/handlers/LedgerEntry.cpp index 5fb99b933..8f2131230 100644 --- a/src/rpc/handlers/LedgerEntry.cpp +++ b/src/rpc/handlers/LedgerEntry.cpp @@ -119,13 +119,12 @@ constexpr auto kHexLocators = std::to_array({ LocatorOrStatus directoryLocator(le::DirectoryEntry const& entry) { - if (entry.dirRoot.has_value() && entry.owner.has_value()) { - return std::unexpected{ - Status{RippledError::RpcInvalidParams, "mayNotSpecifyBothDirRootAndOwner"} - }; + if (entry.dirRoot.has_value() == entry.owner.has_value()) { + return std::unexpected{Status{ + RippledError::RpcInvalidParams, + "Must have exactly one of `owner` and `dir_root` fields." + }}; } - if (not entry.dirRoot.has_value() and not entry.owner.has_value()) - return std::unexpected{Status{RippledError::RpcInvalidParams, "missingOwnerOrDirRoot"}}; auto const subIndex = entry.subIndex.value_or(0); if (entry.dirRoot.has_value()) @@ -140,7 +139,8 @@ depositPreauthLocator(le::DepositPreauthEntry const& entry) // Exactly one of authorized or authorized_credentials MUST exist. if (entry.authorized.has_value() == entry.authorizedCredentials.has_value()) { return std::unexpected{Status{ - ClioError::RpcMalformedRequest, "Must have one of authorized or authorized_credentials." + ClioError::RpcMalformedRequest, + "Must have exactly one of `authorized` and `authorized_credentials`." }}; } diff --git a/src/rpc/handlers/VaultInfo.cpp b/src/rpc/handlers/VaultInfo.cpp index 3e8f614e4..ac99804bc 100644 --- a/src/rpc/handlers/VaultInfo.cpp +++ b/src/rpc/handlers/VaultInfo.cpp @@ -58,9 +58,14 @@ VaultInfoHandler::VaultInfoHandler(std::shared_ptr sharedPtrBa VaultInfoHandler::Result VaultInfoHandler::process(VaultInfoHandler::Input const& input, Context const& ctx) const { - // vault info input must either have owner and sequence, or vault_id only. - if (not validate(input)) - return Error{ClioError::RpcMalformedRequest}; + // vault info input must either have owner and sequence, or vault_id only. Wording and code + // match xrpld's VaultInfo.cpp parseVault(). + if (not validate(input)) { + return Error{Status{ + RippledError::RpcInvalidParams, + "Must specify either 'vault_id' or both 'owner' and 'seq'." + }}; + } auto const range = sharedPtrBackend_->fetchLedgerRange(); ASSERT(range.has_value(), "VaultInfo's ledger range must be available"); diff --git a/tests/common/rpc/FakesAndMocks.hpp b/tests/common/rpc/FakesAndMocks.hpp index 62a0765b2..5505502c8 100644 --- a/tests/common/rpc/FakesAndMocks.hpp +++ b/tests/common/rpc/FakesAndMocks.hpp @@ -1,9 +1,7 @@ #pragma once #include "rpc/Errors.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include #include @@ -24,30 +22,11 @@ namespace tests::common { -// input data for the test handlers below -struct TestInput { - std::string hello; - std::optional limit; -}; - // output data produced by the test handlers below struct TestOutput { std::string computed; }; -// must be implemented as per rpc/common/Concepts.h -inline TestInput -tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) -{ - std::optional optLimit; - if (jv.as_object().contains("limit")) - optLimit = jv.at("limit").as_int64(); - - return { - .hello = boost::json::value_to(jv.as_object().at("hello")), .limit = optLimit - }; -} - // must be implemented as per rpc/common/Concepts.h inline void tag_invoke(boost::json::value_from_tag, boost::json::value& jv, TestOutput const& output) @@ -55,33 +34,6 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, TestOutput const jv = {{"computed", output.computed}}; } -// example handler -class HandlerFake { -public: - using Input = TestInput; - using Output = TestOutput; - using Result = rpc::HandlerReturnType; - - static rpc::RpcSpecConstRef - spec([[maybe_unused]] uint32_t apiVersion) - { - using namespace rpc::validation; - - static auto const kRpcSpec = rpc::RpcSpec{ - {"hello", Required{}, Type{}, EqualTo{"world"}}, - {"limit", Type{}, Between{0, 100}}, // optional field - }; - - return kRpcSpec; - } - - static Result - process(Input input, [[maybe_unused]] rpc::Context const& ctx) - { - return Output{input.hello + '_' + std::to_string(input.limit.value_or(0))}; - } -}; - class NoInputHandlerFake { public: using Output = TestOutput; @@ -94,34 +46,6 @@ public: } }; -// example handler that returns custom error -class FailingHandlerFake { -public: - using Input = TestInput; - using Output = TestOutput; - using Result = rpc::HandlerReturnType; - - static rpc::RpcSpecConstRef - spec([[maybe_unused]] uint32_t apiVersion) - { - using namespace rpc::validation; - - static auto const kRpcSpec = rpc::RpcSpec{ - {"hello", Required{}, Type{}, EqualTo{"world"}}, - {"limit", Type{}, Between{0u, 100u}}, // optional field - }; - - return kRpcSpec; - } - - static Result - process([[maybe_unused]] Input input, [[maybe_unused]] rpc::Context const& ctx) - { - // always fail - return rpc::Error{rpc::Status{"Very custom error"}}; - } -}; - struct InOutFake { std::string something; @@ -144,15 +68,6 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, InOutFake const& jv = {{"something", output.something}}; } -struct HandlerMock { - using Input = InOutFake; - using Output = InOutFake; - using Result = rpc::HandlerReturnType; - - MOCK_METHOD(rpc::RpcSpecConstRef, spec, (uint32_t), (const)); - MOCK_METHOD(Result, process, (Input, rpc::Context const&), (const)); -}; - struct HandlerWithoutInputMock { using Output = InOutFake; using Result = rpc::HandlerReturnType; @@ -164,15 +79,26 @@ struct HandlerWithoutInputMock { // `specFor` hook, so the fake Input below needs its own namespace to host that hook. namespace typed_fake { -// input data for TypedHandlerFake; mirrors TestInput so the two paths stay comparable +// input data for TypedHandlerFake struct TypedInput { std::string hello; std::optional limit; }; inline constexpr auto kInputSpec = rpc::spec::spec( - rpc::spec::field("hello", &TypedInput::hello, rpc::spec::required, rpc::spec::asString), - rpc::spec::field("limit", &TypedInput::limit, rpc::spec::asUint32), + rpc::spec::field( + "hello", + &TypedInput::hello, + rpc::spec::required, + rpc::spec::oneOf("world"), + rpc::spec::asString + ), + rpc::spec::field( + "limit", + &TypedInput::limit, + rpc::spec::between(uint32_t{0}, uint32_t{100}), + rpc::spec::asUint32 + ), rpc::spec::field("old_field", rpc::spec::deprecated) ); diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index c87a454ee..629a7ba87 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -94,12 +94,9 @@ target_sources( migration/SpecTests.cpp # RPC rpc/APIVersionTests.cpp - rpc/BaseTests.cpp rpc/CountersTests.cpp rpc/ErrorTests.cpp rpc/ForwardingProxyTests.cpp - rpc/common/CheckersTests.cpp - rpc/common/SpecsTests.cpp rpc/common/TypesTests.cpp rpc/common/impl/HandlerProviderTests.cpp rpc/filters/impl/DelegateTransactionsFilterTests.cpp @@ -146,7 +143,6 @@ target_sources( rpc/handlers/UnsubscribeTests.cpp rpc/handlers/VersionHandlerTests.cpp rpc/handlers/VaultInfoTests.cpp - rpc/JsonBoolTests.cpp rpc/RPCEngineTests.cpp rpc/RPCHelpersTests.cpp rpc/WorkQueueTests.cpp diff --git a/tests/unit/rpc/BaseTests.cpp b/tests/unit/rpc/BaseTests.cpp deleted file mode 100644 index a1fb3e0ac..000000000 --- a/tests/unit/rpc/BaseTests.cpp +++ /dev/null @@ -1,838 +0,0 @@ -#include "rpc/common/MetaProcessors.hpp" -#include "rpc/common/Modifiers.hpp" -#include "rpc/common/Specs.hpp" -#include "rpc/common/Types.hpp" -#include "rpc/common/ValidationHelpers.hpp" -#include "rpc/common/Validators.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -using namespace std; - -using namespace rpc; -using namespace rpc::validation; -using namespace rpc::meta; -using namespace rpc::modifiers; - -class RPCBaseTest : public virtual ::testing::Test {}; - -TEST_F(RPCBaseTest, CheckTypeString) -{ - auto const jString = boost::json::value("a string"); - ASSERT_TRUE(checkType(jString)); - ASSERT_FALSE(checkType(jString)); -} - -TEST_F(RPCBaseTest, CheckTypeUint) -{ - auto const jUint = boost::json::value(123u); - ASSERT_TRUE(checkType(jUint)); - ASSERT_TRUE(checkType(jUint)); - ASSERT_FALSE(checkType(jUint)); -} - -TEST_F(RPCBaseTest, CheckTypeInt) -{ - auto jInt = boost::json::value(123); - ASSERT_TRUE(checkType(jInt)); - ASSERT_TRUE(checkType(jInt)); - ASSERT_FALSE(checkType(jInt)); - - jInt = boost::json::value(-123); - ASSERT_TRUE(checkType(jInt)); - ASSERT_FALSE(checkType(jInt)); // Unsigned can't be negative - ASSERT_FALSE(checkType(jInt)); -} - -TEST_F(RPCBaseTest, CheckTypeBool) -{ - auto const jBool = boost::json::value(true); - ASSERT_TRUE(checkType(jBool)); - ASSERT_FALSE(checkType(jBool)); -} - -TEST_F(RPCBaseTest, CheckTypeDouble) -{ - auto const jDouble = boost::json::value(0.123); - ASSERT_TRUE(checkType(jDouble)); - ASSERT_TRUE(checkType(jDouble)); - ASSERT_FALSE(checkType(jDouble)); -} - -TEST_F(RPCBaseTest, CheckTypeArray) -{ - auto const jArr = boost::json::value({1, 2, 3}); - ASSERT_TRUE(checkType(jArr)); - ASSERT_FALSE(checkType(jArr)); -} - -TEST_F(RPCBaseTest, CheckTypeAndClampValueUnchanged) -{ - auto jUint = boost::json::value(123u); - ASSERT_TRUE(checkTypeAndClamp(jUint)); - ASSERT_EQ(jUint.as_uint64(), 123u); - ASSERT_TRUE(checkTypeAndClamp(jUint)); - ASSERT_EQ(jUint.as_uint64(), 123u); - - auto jInt = boost::json::value(123); - ASSERT_TRUE(checkTypeAndClamp(jInt)); - ASSERT_EQ(jInt.as_int64(), 123); - ASSERT_TRUE(checkTypeAndClamp(jInt)); - ASSERT_EQ(jInt.as_int64(), 123); - - jInt = boost::json::value(-123); - ASSERT_TRUE(checkTypeAndClamp(jInt)); - ASSERT_EQ(jInt.as_int64(), -123); -} - -TEST_F(RPCBaseTest, CheckTypeAndClampInvalidValues) -{ - auto jInt = boost::json::value(-123); - ASSERT_FALSE(checkTypeAndClamp(jInt)); // Unsigned can't be negative -} - -TEST_F(RPCBaseTest, CheckTypeAndClampOverflow) -{ - auto jBigUint = boost::json::value(std::numeric_limits::max()); - ASSERT_TRUE(checkTypeAndClamp(jBigUint)); - ASSERT_EQ(jBigUint.as_uint64(), std::numeric_limits::max()); - - auto jBigInt = boost::json::value(std::numeric_limits::max()); - ASSERT_TRUE(checkTypeAndClamp(jBigInt)); - ASSERT_EQ(jBigInt.as_int64(), std::numeric_limits::max()); -} - -TEST_F(RPCBaseTest, CheckTypeAndClampUnderflow) -{ - auto jLowInt = boost::json::value(std::numeric_limits::min()); - ASSERT_TRUE(checkTypeAndClamp(jLowInt)); - ASSERT_EQ(jLowInt.as_int64(), std::numeric_limits::min()); -} - -TEST_F(RPCBaseTest, TypeValidator) -{ - auto spec = RpcSpec{ - {"uint", Type{}}, - {"int", Type{}}, - {"str", Type{}}, - {"double", Type{}}, - {"bool", Type{}}, - {"arr", Type{}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ - "uint": 123, - "int": 321, - "str": "a string", - "double": 1.0, - "bool": true, - "arr": [] - })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - { - auto failingInput = boost::json::parse(R"JSON({ "uint": "a string" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - } - { - auto failingInput = boost::json::parse(R"JSON({ "int": "a string" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - } - { - auto failingInput = boost::json::parse(R"JSON({ "str": 1234 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - } - { - auto failingInput = boost::json::parse(R"JSON({ "double": "a string" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - } - { - auto failingInput = boost::json::parse(R"JSON({ "bool": "a string" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - } - { - auto failingInput = boost::json::parse(R"JSON({ "arr": "a string" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - } -} - -TEST_F(RPCBaseTest, TypeValidatorMultipleTypes) -{ - auto spec = RpcSpec{ - // either int or string - {"test", Type{}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "test": "1234" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto passingInput2 = boost::json::parse(R"JSON({ "test": 1234 })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); - - auto failingInput = boost::json::parse(R"JSON({ "test": true })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, RequiredValidator) -{ - auto spec = RpcSpec{ - {"required", Required{}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "required": "present" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto passingInput2 = boost::json::parse(R"JSON({ "required": true })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); - - auto failingInput = boost::json::parse(R"JSON({})JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, BetweenValidator) -{ - auto spec = RpcSpec{ - {"amount", Between{10u, 20u}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "amount": 15 })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto passingInput2 = boost::json::parse(R"JSON({ "amount": 10 })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); - - auto passingInput3 = boost::json::parse(R"JSON({ "amount": 20 })JSON"); - ASSERT_TRUE(spec.process(passingInput3)); - - auto failingInput = boost::json::parse(R"JSON({ "amount": 9 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - auto failingInput2 = boost::json::parse(R"JSON({ "amount": 21 })JSON"); - ASSERT_FALSE(spec.process(failingInput2)); -} - -TEST_F(RPCBaseTest, MinValidator) -{ - auto spec = RpcSpec{ - {"amount", Min{6}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "amount": 7 })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto passingInput2 = boost::json::parse(R"JSON({ "amount": 6 })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); - - auto failingInput = boost::json::parse(R"JSON({ "amount": 5 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, MinValidatorAfterType) -{ - auto spec = RpcSpec{ - {"amount", Type{}, Min{std::numeric_limits::max()}}, - {"amount2", Type{}, Min{std::numeric_limits::max()}}, - {"amount3", Type{}, Min{std::numeric_limits::min()}}, - }; - - auto bigInput = boost::json::parse( - R"JSON({ "amount": 9999999999, "amount2": 9999999999, "amount3": -9999999999 })JSON" - ); - ASSERT_TRUE(spec.process(bigInput)); // type check clamps to type's max/min value -} - -TEST_F(RPCBaseTest, MaxValidator) -{ - auto spec = RpcSpec{ - {"amount", Max{6}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "amount": 5 })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto passingInput2 = boost::json::parse(R"JSON({ "amount": 6 })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); - - auto failingInput = boost::json::parse(R"JSON({ "amount": 7 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, MaxValidatorAfterType) -{ - auto spec = RpcSpec{ - {"amount", Type{}, Max{std::numeric_limits::max()}}, - {"amount2", Type{}, Max{std::numeric_limits::max()}}, - {"amount3", Type{}, Max{std::numeric_limits::min()}}, - }; - - auto bigInput = boost::json::parse( - R"JSON({ "amount": 9999999999, "amount2": 9999999999, "amount3": -9999999999 })JSON" - ); - ASSERT_TRUE(spec.process(bigInput)); // type check clamps to type's min/max value -} - -TEST_F(RPCBaseTest, OneOfValidator) -{ - auto spec = RpcSpec{ - {"currency", OneOf{"XRP", "USD"}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "currency": "XRP" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto passingInput2 = boost::json::parse(R"JSON({ "currency": "USD" })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); - - auto failingInput = boost::json::parse(R"JSON({ "currency": "PRX" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, EqualToValidator) -{ - auto spec = RpcSpec{ - {"exact", EqualTo{"CaseSensitive"}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "exact": "CaseSensitive" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "exact": "Different" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, ArrayAtValidator) -{ - auto spec = RpcSpec{ - {"arr", - Required{}, - Type{}, - ValidateArrayAt{ - 0, - { - {"limit", Required{}, Type{}, Between{0, 100}}, - } - }}, - {"arr2", - ValidateArrayAt{ - 0, - { - {"limit", Required{}, Type{}, Between{0, 100}}, - } - }}, - }; - // clang-format on - - auto passingInput = boost::json::parse(R"JSON({ "arr": [{"limit": 42}] })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "arr": [{"limit": "not int"}] })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = - boost::json::parse(R"JSON({ "arr": [{"limit": 42}], "arr2": "not array type" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "arr": [] })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, IfTypeValidator) -{ - auto spec = RpcSpec{ - {"mix", - Required{}, - Type{}, - IfType{ - Section{{"limit", Required{}, Type{}, Between{0, 100}}}, - Section{{"limit2", Required{}, Type{}, Between{0, 100}}} - }, - IfType{CustomValidators::uint256HexStringValidator}}, - {"mix2", - Section{{"limit", Required{}, Type{}, Between{0, 100}}}, - Type{}}, - }; - - // if json object pass - auto passingInput = boost::json::parse(R"JSON({ "mix": {"limit": 42, "limit2": 22} })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - // if string pass - passingInput = boost::json::parse( - R"JSON({ "mix": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC" })JSON" - ); - ASSERT_TRUE(spec.process(passingInput)); - - // if json object fail at first requirement - auto failingInput = boost::json::parse(R"JSON({ "mix": {"limit": "not int"} })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - // if json object fail at second requirement - failingInput = boost::json::parse(R"JSON({ "mix": {"limit": 22, "limit2": "y"} })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - // if string fail - failingInput = boost::json::parse(R"JSON({ "mix": "not hash" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - // type check fail - failingInput = boost::json::parse(R"JSON({ "mix": 1213 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = - boost::json::parse(R"JSON({ "mix": {"limit": 42, "limit2": 22}, "mix2": 1213 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, WithCustomError) -{ - auto const spec = RpcSpec{ - {"transaction", - WithCustomError{ - CustomValidators::uint256HexStringValidator, - rpc::Status{xrpl::RpcBadFeature, "MyCustomError"} - }}, - {"other", - WithCustomError{ - Type{}, rpc::Status{xrpl::RpcAlreadyMultisig, "MyCustomError2"} - }} - }; - - auto passingInput = boost::json::parse( - R"JSON({ "transaction": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", "other": "1"})JSON" - ); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse( - R"JSON({ "transaction": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515B"})JSON" - ); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "MyCustomError"); - ASSERT_EQ(err.error(), xrpl::RpcBadFeature); - - failingInput = boost::json::parse(R"JSON({ "other": 1})JSON"); - err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "MyCustomError2"); - ASSERT_EQ(err.error(), xrpl::RpcAlreadyMultisig); -} - -TEST_F(RPCBaseTest, TimeFormatValidator) -{ - auto const spec = RpcSpec{ - {"date", TimeFormatValidator{"%Y-%m-%dT%H:%M:%SZ"}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "date": "2023-01-01T00:00:00Z" })JSON"); - EXPECT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse("123"); - EXPECT_TRUE(spec.process(passingInput)); - - // key not exists - passingInput = boost::json::parse(R"JSON({ "date1": "2023-01-01T00:00:00Z" })JSON"); - EXPECT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "date": "2023-01-01-00:00:00" })JSON"); - auto err = spec.process(failingInput); - EXPECT_FALSE(err); - EXPECT_EQ(err.error(), xrpl::RpcInvalidParams); - - failingInput = boost::json::parse(R"JSON({ "date": "01-01-2024T00:00:00" })JSON"); - EXPECT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "date": "2024-01-01T29:00:00" })JSON"); - EXPECT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "date": "" })JSON"); - EXPECT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "date": 1 })JSON"); - err = spec.process(failingInput); - EXPECT_FALSE(err); - EXPECT_EQ(err.error(), xrpl::RpcInvalidParams); -} - -TEST_F(RPCBaseTest, CustomValidator) -{ - auto customFormatCheck = CustomValidator{ - [](boost::json::value const& value, std::string_view /* key */) -> MaybeError { - return value.as_string().size() == 34 ? MaybeError{} : Error{rpc::Status{"Uh oh"}}; - } - }; - - auto spec = RpcSpec{ - {"taker", customFormatCheck}, - }; - - auto passingInput = - boost::json::parse(R"JSON({ "taker": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "taker": "wrongformat" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, NotSupported) -{ - auto spec = RpcSpec{ - {"taker", Type{}, NotSupported{123}}, - {"getter", NotSupported{}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "taker": 2 })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "taker": 123 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "taker": 2, "getter": 2 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, LedgerIndexValidator) -{ - auto spec = RpcSpec{ - {"ledgerIndex", CustomValidators::ledgerIndexValidator}, - }; - auto passingInput = boost::json::parse(R"JSON({ "ledgerIndex": "validated" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse(R"JSON({ "ledgerIndex": "256" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse(R"JSON({ "ledgerIndex": 256 })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "ledgerIndex": "wrongformat" })JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "ledgerIndexMalformed"); - - failingInput = boost::json::parse(R"JSON({ "ledgerIndex": true })JSON"); - err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "ledgerIndexMalformed"); -} - -TEST_F(RPCBaseTest, AccountBase58Validator) -{ - auto spec = RpcSpec{ - {"account", CustomValidators::accountBase58Validator}, - }; - auto failingInput = boost::json::parse(R"JSON({ "account": 256 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = - boost::json::parse(R"JSON({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jp" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse( - R"JSON({ "account": "020000000000000000000000000000000000000000000000000000000000000000" })JSON" - ); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = - boost::json::parse(R"JSON({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jp?" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - auto passingInput = - boost::json::parse(R"JSON({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); -} - -TEST_F(RPCBaseTest, AccountMarkerValidator) -{ - auto spec = RpcSpec{ - {"marker", CustomValidators::accountMarkerValidator}, - }; - auto failingInput = boost::json::parse(R"JSON({ "marker": 256 })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "marker": "testtest" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "marker": "ABAB1234:1H" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - auto passingInput = boost::json::parse(R"JSON({ "account": "ABAB1234:123" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); -} - -TEST_F(RPCBaseTest, Uint160HexStringValidator) -{ - auto const spec = RpcSpec{{"marker", CustomValidators::uint160HexStringValidator}}; - auto passingInput = - boost::json::parse(R"JSON({ "marker": "F609A18102218C75767209946A77523CBD97E225"})JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "marker": 160})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "markerNotString"); - - failingInput = boost::json::parse( - R"JSON({ "marker": "F609A18102218C75767209946A77523CBD97E2253515BC"})JSON" - ); - err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "markerMalformed"); -} - -TEST_F(RPCBaseTest, Uint192HexStringValidator) -{ - auto const spec = RpcSpec{{"mpt_issuance_id", CustomValidators::uint192HexStringValidator}}; - auto passingInput = boost::json::parse( - R"JSON({ "mpt_issuance_id": "0000012F27A9DE73EAA1E8831FA253E19030A17E2D038198"})JSON" - ); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "mpt_issuance_id": 192})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "mpt_issuance_idNotString"); - - failingInput = boost::json::parse( - R"JSON({ "mpt_issuance_id": "0000012F27A9DE73EAA1E8831FA253E19030A17E2D038198983515BC"})JSON" - ); - err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "mpt_issuance_idMalformed"); -} - -TEST_F(RPCBaseTest, Uint256HexStringValidator) -{ - auto const spec = RpcSpec{{"transaction", CustomValidators::uint256HexStringValidator}}; - auto passingInput = boost::json::parse( - R"JSON({ "transaction": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"})JSON" - ); - ASSERT_TRUE(spec.process(passingInput)); - - auto failingInput = boost::json::parse(R"JSON({ "transaction": 256})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "transactionNotString"); - - failingInput = boost::json::parse( - R"JSON({ "transaction": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC"})JSON" - ); - err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "transactionMalformed"); -} - -TEST_F(RPCBaseTest, CurrencyValidator) -{ - auto const spec = RpcSpec{{"currency", CustomValidators::currencyValidator}}; - auto passingInput = boost::json::parse(R"JSON({ "currency": "GBP"})JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = - boost::json::parse(R"JSON({ "currency": "0158415500000000C1F76FF6ECB0BAC600000000"})JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = - boost::json::parse(R"JSON({ "currency": "0158415500000000c1f76ff6ecb0bac600000000"})JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - for (auto const& currency : {"[]<", ">()", "{}|", "?!@", "#$%", "^&*"}) { - passingInput = - boost::json::parse(fmt::format(R"JSON({{ "currency": "{}" }})JSON", currency)); - ASSERT_TRUE(spec.process(passingInput)); - } - - auto failingInput = boost::json::parse(R"JSON({ "currency": 256})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "currencyNotString"); - - failingInput = boost::json::parse(R"JSON({ "currency": "12314"})JSON"); - err = spec.process(failingInput); - ASSERT_FALSE(err); - ASSERT_EQ(err.error().message, "malformedCurrency"); -} - -TEST_F(RPCBaseTest, BookTakerValidator) -{ - auto const spec = RpcSpec{{"taker_gets", CustomValidators::bookTakerValidator}}; - - // An asset specified by currency, optionally with an issuer, is valid. - auto passingInput = boost::json::parse(R"JSON({ "taker_gets": { "currency": "XRP" }})JSON"); - EXPECT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse( - R"JSON({ "taker_gets": { "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" }})JSON" - ); - EXPECT_TRUE(spec.process(passingInput)); - - // An asset specified by mpt_issuance_id alone is valid. - passingInput = boost::json::parse( - R"JSON({ "taker_gets": { "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD" }})JSON" - ); - EXPECT_TRUE(spec.process(passingInput)); - - // Non-object values are deferred to the type validator; the field being absent is deferred to - // the required validator. Both pass here. - passingInput = boost::json::parse(R"JSON({ "taker_gets": "not_an_object" })JSON"); - EXPECT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse(R"JSON({ })JSON"); - EXPECT_TRUE(spec.process(passingInput)); - - // Neither currency nor mpt_issuance_id -> invalidParams, reporting the currency field. - auto failingInput = boost::json::parse(R"JSON({ "taker_gets": { }})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams}); - EXPECT_EQ(err.error().message, "Missing field 'taker_gets.currency'."); - - // currency and mpt_issuance_id are mutually exclusive. - failingInput = boost::json::parse( - R"JSON({ "taker_gets": { "currency": "USD", "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD" }})JSON" - ); - err = spec.process(failingInput); - ASSERT_FALSE(err); - EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams}); - EXPECT_EQ(err.error().message, "Invalid field 'taker_gets'."); - - // mpt_issuance_id must not be combined with an issuer. - failingInput = boost::json::parse( - R"JSON({ "taker_gets": { "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" }})JSON" - ); - err = spec.process(failingInput); - ASSERT_FALSE(err); - EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams}); - EXPECT_EQ(err.error().message, "Invalid field 'taker_gets'."); - - // A present-but-non-string currency -> expectedFieldError against '.currency'. - failingInput = boost::json::parse(R"JSON({ "taker_gets": { "currency": 123 }})JSON"); - err = spec.process(failingInput); - ASSERT_FALSE(err); - EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams}); - EXPECT_EQ(err.error().message, "Invalid field 'taker_gets.currency', not string."); - - // A present-but-non-string mpt_issuance_id is reported the same way (against '.currency', - // matching xrpld's validateTakerJSON). - failingInput = boost::json::parse(R"JSON({ "taker_gets": { "mpt_issuance_id": 123 }})JSON"); - err = spec.process(failingInput); - ASSERT_FALSE(err); - EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams}); - EXPECT_EQ(err.error().message, "Invalid field 'taker_gets.currency', not string."); -} - -TEST_F(RPCBaseTest, BookTakerValidatorUsesFieldKey) -{ - // The error messages must reflect the validated field's key (taker_pays vs taker_gets). - auto const spec = RpcSpec{{"taker_pays", CustomValidators::bookTakerValidator}}; - - auto failingInput = boost::json::parse(R"JSON({ "taker_pays": { }})JSON"); - auto err = spec.process(failingInput); - ASSERT_FALSE(err); - EXPECT_EQ(err.error().message, "Missing field 'taker_pays.currency'."); - - failingInput = boost::json::parse( - R"JSON({ "taker_pays": { "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" }})JSON" - ); - err = spec.process(failingInput); - ASSERT_FALSE(err); - EXPECT_EQ(err.error().message, "Invalid field 'taker_pays'."); -} - -TEST_F(RPCBaseTest, ClampingModifier) -{ - auto spec = RpcSpec{ - {"amount", Clamp{10u, 20u}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "amount": 15 })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - auto passingInput2 = boost::json::parse(R"JSON({ "amount": 5 })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); - ASSERT_EQ(passingInput2.at("amount").as_uint64(), 10u); // clamped - - auto passingInput3 = boost::json::parse(R"JSON({ "amount": 25 })JSON"); - ASSERT_TRUE(spec.process(passingInput3)); - ASSERT_EQ(passingInput3.at("amount").as_uint64(), 20u); // clamped -} - -TEST_F(RPCBaseTest, ToLowerModifier) -{ - auto spec = RpcSpec{ - {"str", ToLower{}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "str": "TesT" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - ASSERT_EQ(passingInput.at("str").as_string(), "test"); - - auto passingInput2 = boost::json::parse(R"JSON({ "str2": "TesT" })JSON"); - ASSERT_TRUE(spec.process(passingInput2)); // no str no problem - - auto passingInput3 = boost::json::parse(R"JSON({ "str": "already lower case" })JSON"); - ASSERT_TRUE(spec.process(passingInput3)); - ASSERT_EQ(passingInput3.at("str").as_string(), "already lower case"); - - auto passingInput4 = boost::json::parse(R"JSON({ "str": "" })JSON"); - ASSERT_TRUE(spec.process(passingInput4)); // empty str no problem - ASSERT_EQ(passingInput4.at("str").as_string(), ""); -} - -TEST_F(RPCBaseTest, ToNumberModifier) -{ - auto const spec = RpcSpec{ - {"str", ToNumber{}}, - }; - - auto passingInput = boost::json::parse(R"JSON({ "str": [] })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse(R"JSON({ "str2": "TesT" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse(R"JSON([])JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse(R"JSON({ "str": "123" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - ASSERT_EQ(passingInput.at("str").as_int64(), 123); - - auto failingInput = boost::json::parse(R"JSON({ "str": "ok" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); - - failingInput = boost::json::parse(R"JSON({ "str": "123.123" })JSON"); - ASSERT_FALSE(spec.process(failingInput)); -} - -TEST_F(RPCBaseTest, CustomModifier) -{ - testing::StrictMock< - testing::MockFunction> - mockModifier; - auto const customModifier = CustomModifier{mockModifier.AsStdFunction()}; - auto const spec = RpcSpec{ - {"str", customModifier}, - }; - - EXPECT_CALL(mockModifier, Call).WillOnce(testing::Return(MaybeError{})); - auto passingInput = boost::json::parse(R"JSON({ "str": "sss" })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - passingInput = boost::json::parse(R"JSON({ "strNotExist": 123 })JSON"); - ASSERT_TRUE(spec.process(passingInput)); - - // not a json object - passingInput = boost::json::parse(R"JSON([])JSON"); - ASSERT_TRUE(spec.process(passingInput)); -} diff --git a/tests/unit/rpc/ForwardingProxyTests.cpp b/tests/unit/rpc/ForwardingProxyTests.cpp index a8cac87ea..87927d50d 100644 --- a/tests/unit/rpc/ForwardingProxyTests.cpp +++ b/tests/unit/rpc/ForwardingProxyTests.cpp @@ -119,6 +119,17 @@ generateTestValuesForParametersTest() .called = 1, .isAdmin = isAdmin, .expected = shouldForward}, + // The isClioOnly check short-circuits before the current/closed check, so a Clio-only + // method is NOT forwarded even when it names a ledger Clio does not hold. Its handler has + // to reject the shortcut itself - see getLedgerHeaderFromLedgerSpecifier. + {.testName = "ShouldForwardReturnsFalseIfClioOnlyEvenWithCurrentLedger", + .apiVersion = 2u, + .method = "nft_info", + .testJson = R"JSON({"ledger_index": "current"})JSON", + .mockedIsClioOnly = isClioOnly, + .called = 1, + .isAdmin = !isAdmin, + .expected = !shouldForward}, {.testName = "ShouldForwardReturnsTrueIfCurrentLedgerSpecified", .apiVersion = 2u, .method = "anymethod", diff --git a/tests/unit/rpc/JsonBoolTests.cpp b/tests/unit/rpc/JsonBoolTests.cpp deleted file mode 100644 index f3805b3d3..000000000 --- a/tests/unit/rpc/JsonBoolTests.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include "util/NameGenerator.hpp" - -#include -#include -#include -#include - -#include -#include - -using namespace rpc; -using namespace testing; - -struct JsonBoolTestsCaseBundle { - std::string testName; - std::string json; - bool expectedBool; -}; - -class JsonBoolTests : public TestWithParam { -public: - static auto - generateTestValuesForParametersTest() - { - return std::vector{ - {.testName = "NullValue", - .json = R"JSON({ "test_bool": null })JSON", - .expectedBool = false}, - {.testName = "BoolTrueValue", - .json = R"JSON({ "test_bool": true })JSON", - .expectedBool = true}, - {.testName = "BoolFalseValue", - .json = R"JSON({ "test_bool": false })JSON", - .expectedBool = false}, - {.testName = "IntTrueValue", - .json = R"JSON({ "test_bool": 1 })JSON", - .expectedBool = true}, - {.testName = "IntFalseValue", - .json = R"JSON({ "test_bool": 0 })JSON", - .expectedBool = false}, - {.testName = "DoubleTrueValue", - .json = R"JSON({ "test_bool": 0.1 })JSON", - .expectedBool = true}, - {.testName = "DoubleFalseValue", - .json = R"JSON({ "test_bool": 0.0 })JSON", - .expectedBool = false}, - {.testName = "StringTrueValue", - .json = R"JSON({ "test_bool": "true" })JSON", - .expectedBool = true}, - {.testName = "StringFalseValue", - .json = R"JSON({ "test_bool": "false" })JSON", - .expectedBool = true}, - {.testName = "ArrayTrueValue", - .json = R"JSON({ "test_bool": [0] })JSON", - .expectedBool = true}, - {.testName = "ArrayFalseValue", - .json = R"JSON({ "test_bool": [] })JSON", - .expectedBool = false}, - {.testName = "ObjectTrueValue", - .json = R"JSON({ "test_bool": { "key": null } })JSON", - .expectedBool = true}, - {.testName = "ObjectFalseValue", - .json = R"JSON({ "test_bool": {} })JSON", - .expectedBool = false} - }; - } -}; - -INSTANTIATE_TEST_CASE_P( - JsonBoolCheckGroup, - JsonBoolTests, - ValuesIn(JsonBoolTests::generateTestValuesForParametersTest()), - tests::util::kNameGenerator -); - -TEST_P(JsonBoolTests, Parse) -{ - auto const testBundle = GetParam(); - auto const jv = boost::json::parse(testBundle.json).as_object(); - ASSERT_TRUE(jv.contains("test_bool")); - EXPECT_EQ(testBundle.expectedBool, value_to(jv.at("test_bool")).value); -} diff --git a/tests/unit/rpc/RPCEngineTests.cpp b/tests/unit/rpc/RPCEngineTests.cpp index 840e05c5c..0bfae0b62 100644 --- a/tests/unit/rpc/RPCEngineTests.cpp +++ b/tests/unit/rpc/RPCEngineTests.cpp @@ -220,12 +220,12 @@ TEST_P(RPCEngineFlowParameterTest, Test) } else { if (testBundle.handlerReturnError) { EXPECT_CALL(*handlerProvider, getHandler) - .WillOnce(Return(AnyHandler{tests::common::FailingHandlerFake{}})); + .WillOnce(Return(AnyHandler{tests::common::FailingTypedHandlerFake{}})); EXPECT_CALL(*mockCountersPtr_, rpcErrored(testBundle.method)); EXPECT_CALL(*handlerProvider, contains(testBundle.method)).WillOnce(Return(true)); } else { EXPECT_CALL(*handlerProvider, getHandler(testBundle.method)) - .WillOnce(Return(AnyHandler{tests::common::HandlerFake{}})); + .WillOnce(Return(AnyHandler{tests::common::TypedHandlerFake{}})); } } } @@ -261,7 +261,7 @@ TEST_F(RPCEngineTest, ThrowDatabaseError) ); EXPECT_CALL(*backend_, isTooBusy).WillOnce(Return(false)); EXPECT_CALL(*handlerProvider, getHandler(method)) - .WillOnce(Return(AnyHandler{tests::common::FailingHandlerFake{}})); + .WillOnce(Return(AnyHandler{tests::common::FailingTypedHandlerFake{}})); EXPECT_CALL(*mockCountersPtr_, rpcErrored(method)).WillOnce(Throw(data::DatabaseError{})); EXPECT_CALL(*handlerProvider, contains(method)).WillOnce(Return(true)); EXPECT_CALL(*mockCountersPtr_, onTooBusy()); @@ -293,7 +293,7 @@ TEST_F(RPCEngineTest, ThrowException) ); EXPECT_CALL(*backend_, isTooBusy).WillOnce(Return(false)); EXPECT_CALL(*handlerProvider, getHandler(method)) - .WillOnce(Return(AnyHandler{tests::common::FailingHandlerFake{}})); + .WillOnce(Return(AnyHandler{tests::common::FailingTypedHandlerFake{}})); EXPECT_CALL(*mockCountersPtr_, rpcErrored(method)).WillOnce(Throw(std::exception{})); EXPECT_CALL(*handlerProvider, contains(method)).WillOnce(Return(true)); EXPECT_CALL(*mockCountersPtr_, onInternalError()); @@ -486,7 +486,7 @@ TEST_F(RPCEngineTest, NonBareRequestBypassesCache) EXPECT_CALL(*backend_, isTooBusy).Times(callTime).WillRepeatedly(Return(false)); EXPECT_CALL(*handlerProvider, getHandler) .Times(callTime) - .WillRepeatedly(Return(AnyHandler{tests::common::HandlerFake{}})); + .WillRepeatedly(Return(AnyHandler{tests::common::TypedHandlerFake{}})); while (callTime-- != 0) { runSpawn([&](auto yield) { @@ -538,7 +538,7 @@ TEST_F(RPCEngineTest, NotCacheIfErrorHappen) EXPECT_CALL(*backend_, isTooBusy).Times(callTime).WillRepeatedly(Return(false)); EXPECT_CALL(*handlerProvider, getHandler) .Times(callTime) - .WillRepeatedly(Return(AnyHandler{tests::common::FailingHandlerFake{}})); + .WillRepeatedly(Return(AnyHandler{tests::common::FailingTypedHandlerFake{}})); EXPECT_CALL(*mockCountersPtr_, rpcErrored(method)).Times(callTime); EXPECT_CALL(*handlerProvider, isClioOnly).Times(callTime).WillRepeatedly(Return(false)); EXPECT_CALL(*handlerProvider, contains).Times(callTime).WillRepeatedly(Return(true)); diff --git a/tests/unit/rpc/RPCHelpersTests.cpp b/tests/unit/rpc/RPCHelpersTests.cpp index 0529adbc6..303637c07 100644 --- a/tests/unit/rpc/RPCHelpersTests.cpp +++ b/tests/unit/rpc/RPCHelpersTests.cpp @@ -7,7 +7,6 @@ #include "util/AsioContextTestFixture.hpp" #include "util/LoggerFixtures.hpp" #include "util/MockAmendmentCenter.hpp" -#include "util/MockAssert.hpp" #include "util/MockBackendTestFixture.hpp" #include "util/MockPrometheus.hpp" #include "util/NameGenerator.hpp" @@ -2051,43 +2050,40 @@ TEST_F(RPCHelpersTest, LedgerHeaderFromSpecifierValidatedUsesMaxSeq) }); } -struct RPCHelpersAssertTest : RPCHelpersTest, common::util::WithMockAssert {}; - -TEST_F(RPCHelpersAssertTest, LedgerHeaderFromSpecifierCurrentAsserts) +// `current` and `closed` name ledgers Clio does not hold. Most methods never reach here because +// ForwardingProxy diverts them to xrpld, but it skips that check for Clio-only methods, so the +// resolver has to reject the shortcut rather than treat it as unreachable. +TEST_F(RPCHelpersTest, LedgerHeaderFromSpecifierCurrentRejected) { EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(0); runSpawn([&, this](auto yield) { - EXPECT_CLIO_ASSERT_FAIL_WITH_MESSAGE( - { - [[maybe_unused]] auto const res = getLedgerHeaderFromLedgerSpecifier( - *backend_, - yield, - rpc::spec::LedgerSpecifier{rpc::spec::LedgerShortcut::Current}, - kSpecifierRangeMax - ); - }, - "must be forwarded before dispatch" + auto const res = getLedgerHeaderFromLedgerSpecifier( + *backend_, + yield, + rpc::spec::LedgerSpecifier{rpc::spec::LedgerShortcut::Current}, + kSpecifierRangeMax ); + ASSERT_FALSE(res.has_value()); + EXPECT_EQ(res.error().code, rpc::CombinedError{rpc::RippledError::RpcInvalidParams}); + EXPECT_EQ(res.error().message, "ledgerIndexMalformed"); }); } -TEST_F(RPCHelpersAssertTest, LedgerHeaderFromSpecifierClosedAsserts) +TEST_F(RPCHelpersTest, LedgerHeaderFromSpecifierClosedRejected) { EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(0); runSpawn([&, this](auto yield) { - EXPECT_CLIO_ASSERT_FAIL_WITH_MESSAGE( - { - [[maybe_unused]] auto const res = getLedgerHeaderFromLedgerSpecifier( - *backend_, - yield, - rpc::spec::LedgerSpecifier{rpc::spec::LedgerShortcut::Closed}, - kSpecifierRangeMax - ); - }, - "must be forwarded before dispatch" + auto const res = getLedgerHeaderFromLedgerSpecifier( + *backend_, + yield, + rpc::spec::LedgerSpecifier{rpc::spec::LedgerShortcut::Closed}, + kSpecifierRangeMax ); + ASSERT_FALSE(res.has_value()); + EXPECT_EQ(res.error().code, rpc::CombinedError{rpc::RippledError::RpcInvalidParams}); + EXPECT_EQ(res.error().message, "ledgerIndexMalformed"); }); } diff --git a/tests/unit/rpc/common/CheckersTests.cpp b/tests/unit/rpc/common/CheckersTests.cpp deleted file mode 100644 index a9c99d899..000000000 --- a/tests/unit/rpc/common/CheckersTests.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "rpc/common/Checkers.hpp" - -#include -#include -#include - -#include - -using namespace rpc; -using namespace rpc::check; - -struct DeprecatedTests : ::testing::Test { - boost::json::value const json{ - {"some_string", "some_value"}, - {"some_number", 42}, - {"some_bool", false}, - {"some_float", 3.14} - }; -}; - -TEST_F(DeprecatedTests, Field) -{ - auto warning = Deprecated<>::check(json, "some_string"); - ASSERT_TRUE(warning.has_value()); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated); - - warning = Deprecated<>::check(json, "other"); - EXPECT_FALSE(warning.has_value()); -} - -TEST_F(DeprecatedTests, FieldWithStringValue) -{ - Deprecated const checker{"some_value"}; - auto warning = checker.check(json, "some_string"); - ASSERT_TRUE(warning.has_value()); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->extraMessage, "Value 'some_value' for field 'some_string' is deprecated"); - EXPECT_FALSE(Deprecated{"other"}.check(json, "some_string")); -} - -TEST_F(DeprecatedTests, FieldWithIntValue) -{ - Deprecated const checker{42}; - auto warning = checker.check(json, "some_number"); - ASSERT_TRUE(warning.has_value()); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->extraMessage, "Value '42' for field 'some_number' is deprecated"); - EXPECT_FALSE(Deprecated{43}.check(json, "some_number")); -} - -TEST_F(DeprecatedTests, FieldWithBoolValue) -{ - Deprecated const checker{false}; - auto warning = checker.check(json, "some_bool"); - ASSERT_TRUE(warning.has_value()); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->extraMessage, "Value 'false' for field 'some_bool' is deprecated"); - EXPECT_FALSE(Deprecated{true}.check(json, "some_bool")); -} - -TEST_F(DeprecatedTests, FieldWithFloatValue) -{ - Deprecated const checker{3.14}; - auto warning = checker.check(json, "some_float"); - ASSERT_TRUE(warning.has_value()); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - EXPECT_EQ(warning->extraMessage, "Value '3.14' for field 'some_float' is deprecated"); - EXPECT_FALSE(Deprecated{3.15}.check(json, "some_float")); -} diff --git a/tests/unit/rpc/common/SpecsTests.cpp b/tests/unit/rpc/common/SpecsTests.cpp deleted file mode 100644 index 620a22b7c..000000000 --- a/tests/unit/rpc/common/SpecsTests.cpp +++ /dev/null @@ -1,284 +0,0 @@ -#include "rpc/common/Checkers.hpp" -#include "rpc/common/Specs.hpp" -#include "rpc/common/Types.hpp" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -using namespace rpc; - -using testing::StrictMock; - -struct SpecsTests : testing::Test { - struct RequirementMock { - MOCK_METHOD(MaybeError, verify, (boost::json::value const&, std::string)); - }; - - struct RequirementMockRef { - RequirementMockRef(StrictMock& ref) : ref(ref) - { - } - MaybeError - verify(boost::json::value& value, std::string key) const - { - return ref.verify(value, key); - } - - StrictMock& ref; - }; - - struct CheckMock { - MOCK_METHOD(std::optional, check, (boost::json::value const&, std::string)); - }; - - struct CheckMockRef { - CheckMockRef(StrictMock& ref) : ref(ref) - { - } - - [[nodiscard]] std::optional - check(boost::json::value const& value, std::string key) const - { - return ref.check(value, key); - } - - StrictMock& ref; - }; - - StrictMock requirementMock; - StrictMock anotherRequirementMock; - StrictMock checkMock; - StrictMock anotherCheckMock; -}; - -struct ProcessorTestBundle { - std::string name; - MaybeError requirementResult; - std::optional otherRequirementResult; - MaybeError expectedResult; -}; - -struct FieldProcessorTests : SpecsTests, testing::WithParamInterface { -protected: - FieldSpec spec_{ - "key", - RequirementMockRef(requirementMock), - RequirementMockRef(anotherRequirementMock) - }; - boost::json::value json_; -}; - -INSTANTIATE_TEST_SUITE_P( - FieldSpecTestGroup, - FieldProcessorTests, - testing::Values( - ProcessorTestBundle{"NoErrors", MaybeError{}, MaybeError{}, MaybeError{}}, - ProcessorTestBundle{ - "FirstError", - Error{Status{"error1"}}, - std::nullopt, - Error{Status{"error1"}}, - }, - ProcessorTestBundle{ - "SecondError", - MaybeError{}, - Error{Status{"error2"}}, - Error{Status{"error2"}}, - } - ), - [](testing::TestParamInfo const& info) { return info.param.name; } -); - -TEST_P(FieldProcessorTests, FieldSpecWithRequirementProcess) -{ - EXPECT_CALL(requirementMock, verify).WillOnce(testing::Return(GetParam().requirementResult)); - if (GetParam().otherRequirementResult.has_value()) { - EXPECT_CALL(anotherRequirementMock, verify) - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - .WillOnce(testing::Return(GetParam().otherRequirementResult.value())); - } - - auto const result = spec_.process(json_); - EXPECT_EQ(result, GetParam().expectedResult); -} - -TEST_F(FieldProcessorTests, FieldSpecWithRequirementCheck) -{ - auto const result = spec_.check(json_); - EXPECT_EQ(result, check::Warnings{}); -} - -struct FieldCheckerTestBundle { - std::string name; - std::optional checkResult; - std::optional otherCheckResult; - check::Warnings expectedWarnings; -}; - -struct FieldCheckerTests : SpecsTests, testing::WithParamInterface { -protected: - FieldSpec spec_{"key", CheckMockRef(checkMock), CheckMockRef(anotherCheckMock)}; - boost::json::value json_; -}; - -INSTANTIATE_TEST_SUITE_P( - FieldSpecTestGroup, - FieldCheckerTests, - testing::Values( - FieldCheckerTestBundle{"NoWarnings", std::nullopt, std::nullopt, check::Warnings{}}, - FieldCheckerTestBundle{ - "FirstWarning", - check::Warning{WarningCode::WarnUnknown, "error1"}, - std::nullopt, - check::Warnings{check::Warning{WarningCode::WarnUnknown, "error1"}} - }, - FieldCheckerTestBundle{ - "SecondWarning", - std::nullopt, - check::Warning{WarningCode::WarnUnknown, "error2"}, - check::Warnings{check::Warning{WarningCode::WarnUnknown, "error2"}} - }, - FieldCheckerTestBundle{ - "BothWarnings", - check::Warning{WarningCode::WarnUnknown, "error1"}, - check::Warning{WarningCode::WarnUnknown, "error2"}, - check::Warnings{ - check::Warning{WarningCode::WarnUnknown, "error1"}, - check::Warning{WarningCode::WarnUnknown, "error2"} - } - } - ), - [](testing::TestParamInfo const& info) { return info.param.name; } -); - -TEST_F(FieldCheckerTests, FieldSpecWithCheckProcess) -{ - auto const result = spec_.process(json_); - EXPECT_EQ(result, MaybeError{}); -} - -TEST_P(FieldCheckerTests, FieldSpecWithCheck) -{ - EXPECT_CALL(checkMock, check).WillOnce(testing::Return(GetParam().checkResult)); - EXPECT_CALL(anotherCheckMock, check).WillOnce(testing::Return(GetParam().otherCheckResult)); - auto const result = spec_.check(json_); - EXPECT_EQ(result, GetParam().expectedWarnings); -} - -struct RpcSpecProcessTests : SpecsTests, testing::WithParamInterface { - RpcSpec spec{ - {"key1", RequirementMockRef(requirementMock)}, - {"key2", RequirementMockRef(anotherRequirementMock)} - }; - boost::json::value json; -}; - -INSTANTIATE_TEST_SUITE_P( - RpcSpecProcessTestGroup, - RpcSpecProcessTests, - testing::Values( - ProcessorTestBundle{"NoErrors", MaybeError{}, MaybeError{}, MaybeError{}}, - ProcessorTestBundle{ - "FirstError", - Error{Status{"error1"}}, - std::nullopt, - Error{Status{"error1"}}, - }, - ProcessorTestBundle{ - "SecondError", - MaybeError{}, - Error{Status{"error2"}}, - Error{Status{"error2"}}, - } - ), - [](testing::TestParamInfo const& info) { return info.param.name; } -); - -TEST_P(RpcSpecProcessTests, Process) -{ - EXPECT_CALL(requirementMock, verify).WillOnce(testing::Return(GetParam().requirementResult)); - if (GetParam().otherRequirementResult.has_value()) { - EXPECT_CALL(anotherRequirementMock, verify) - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - .WillOnce(testing::Return(GetParam().otherRequirementResult.value())); - } - auto const result = spec.process(json); - EXPECT_EQ(result, GetParam().expectedResult); -} - -struct RpcSpecCheckTestBundle { - std::string name; - std::optional checkResult; - std::optional otherCheckResult; - std::unordered_map> expectedWarnings; -}; - -struct RpcSpecCheckTests : SpecsTests, testing::WithParamInterface { -protected: - RpcSpec spec_{{"key1", CheckMockRef(checkMock)}, {"key2", CheckMockRef(anotherCheckMock)}}; - boost::json::value json_; -}; - -INSTANTIATE_TEST_SUITE_P( - RpcSpecCheckTestGroup, - RpcSpecCheckTests, - testing::Values( - RpcSpecCheckTestBundle{"NoWarnings", std::nullopt, std::nullopt, {}}, - RpcSpecCheckTestBundle{ - "FirstWarning", - check::Warning{WarningCode::WarnUnknown, "error1"}, - std::nullopt, - {{WarningCode::WarnUnknown, {"error1"}}} - }, - RpcSpecCheckTestBundle{ - "SecondWarning", - std::nullopt, - check::Warning{WarningCode::WarnUnknown, "error2"}, - {{WarningCode::WarnUnknown, {"error2"}}} - }, - RpcSpecCheckTestBundle{ - "BothWarnings", - check::Warning{WarningCode::WarnUnknown, "error1"}, - check::Warning{WarningCode::WarnUnknown, "error2"}, - {{WarningCode::WarnUnknown, {"error1", "error2"}}} - }, - RpcSpecCheckTestBundle{ - "DifferentWarningCodes", - check::Warning{WarningCode::WarnUnknown, "error1"}, - check::Warning{WarningCode::WarnRpcClio, "error2"}, - {{WarningCode::WarnUnknown, {"error1"}}, {WarningCode::WarnRpcClio, {"error2"}}} - } - - ), - [](testing::TestParamInfo const& info) { return info.param.name; } -); - -TEST_P(RpcSpecCheckTests, Check) -{ - EXPECT_CALL(checkMock, check).WillOnce(testing::Return(GetParam().checkResult)); - EXPECT_CALL(anotherCheckMock, check).WillOnce(testing::Return(GetParam().otherCheckResult)); - - auto const result = spec_.check(json_); - ASSERT_EQ(result.size(), GetParam().expectedWarnings.size()); - for (auto const& entry : result) { - ASSERT_TRUE(entry.is_object()); - auto const& object = entry.as_object(); - ASSERT_TRUE(object.contains("id")); - ASSERT_TRUE(object.at("id").is_int64()); - ASSERT_TRUE(object.contains("message")); - ASSERT_TRUE(object.at("message").is_string()); - auto it = GetParam().expectedWarnings.find(object.at("id").as_int64()); - ASSERT_NE(it, GetParam().expectedWarnings.end()); - for (auto const& message : it->second) { - EXPECT_NE(object.at("message").as_string().find(message), std::string::npos); - } - } -} diff --git a/tests/unit/rpc/handlers/AccountMPTokenIssuancesTests.cpp b/tests/unit/rpc/handlers/AccountMPTokenIssuancesTests.cpp index 346e63bc8..ad4b0beeb 100644 --- a/tests/unit/rpc/handlers/AccountMPTokenIssuancesTests.cpp +++ b/tests/unit/rpc/handlers/AccountMPTokenIssuancesTests.cpp @@ -2,7 +2,6 @@ #include "rpc/Errors.hpp" #include "rpc/common/AnyHandler.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include "rpc/handlers/AccountMPTokenIssuances.hpp" #include "util/HandlerBaseTestFixture.hpp" #include "util/NameGenerator.hpp" @@ -1213,11 +1212,8 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MPTokenIssuanceIdIsDerivedIdNotLed EXPECT_EQ(mptIssuanceId, expectedMptIssuanceId(kSequence)); EXPECT_NE(mptIssuanceId, kIssuanceIndex1); - auto const asOuterObject = boost::json::value{{"mpt_issuance_id", mptIssuanceId}}; - auto const validated = validation::CustomValidators::uint192HexStringValidator.verify( - asOuterObject, "mpt_issuance_id" - ); - EXPECT_TRUE(validated.has_value()); + xrpl::uint192 parsedMptIssuanceId; + EXPECT_TRUE(parsedMptIssuanceId.parseHex(mptIssuanceId)); }); } diff --git a/tests/unit/rpc/handlers/DefaultProcessorTests.cpp b/tests/unit/rpc/handlers/DefaultProcessorTests.cpp index 5c0db6296..6295435d1 100644 --- a/tests/unit/rpc/handlers/DefaultProcessorTests.cpp +++ b/tests/unit/rpc/handlers/DefaultProcessorTests.cpp @@ -1,9 +1,7 @@ #include "rpc/Errors.hpp" #include "rpc/FakesAndMocks.hpp" #include "rpc/common/Concepts.hpp" -#include "rpc/common/Specs.hpp" #include "rpc/common/Types.hpp" -#include "rpc/common/Validators.hpp" #include "rpc/common/impl/Processors.hpp" #include "util/HandlerBaseTestFixture.hpp" @@ -15,30 +13,15 @@ using namespace testing; using namespace std; using namespace rpc; -using namespace rpc::validation; using namespace tests::common; class RPCDefaultProcessorTest : public HandlerBaseTest {}; -TEST_F(RPCDefaultProcessorTest, ValidInput) -{ - runSpawn([](auto yield) { - HandlerMock const handler; - rpc::impl::DefaultProcessor const processor; +static_assert(SomeHandlerWithTypedInput); +static_assert(SomeHandlerWithTypedInput); +static_assert(SomeHandlerWithoutInput); - auto const input = boost::json::parse(R"JSON({ "something": "works" })JSON"); - 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, Context{yield}); - ASSERT_TRUE(ret); // no error - EXPECT_TRUE(ret.warnings.empty()); - }); -} - -TEST_F(RPCDefaultProcessorTest, NoInputValidCall) +TEST_F(RPCDefaultProcessorTest, NoInputHandler_ValidCall) { runSpawn([](auto yield) { HandlerWithoutInputMock const handler; @@ -54,34 +37,10 @@ TEST_F(RPCDefaultProcessorTest, NoInputValidCall) }); } -TEST_F(RPCDefaultProcessorTest, InvalidInput) -{ - runSpawn([](auto yield) { - HandlerMock const handler; - rpc::impl::DefaultProcessor const processor; +// These exercise the input path of a handler whose spec, validation and +// deserialization all come from the shared consteval spec via HandlerFor. - auto const input = boost::json::parse(R"JSON({ "other": "nope" })JSON"); - auto const spec = RpcSpec{{"something", Required{}}}; - EXPECT_CALL(handler, spec(_)).WillOnce(ReturnRef(spec)); - - auto const ret = processor(handler, input, Context{yield}); - ASSERT_FALSE(ret); // returns error - EXPECT_TRUE(ret.warnings.empty()); - }); -} - -static_assert(SomeHandlerWithTypedInput); -static_assert(not SomeHandlerWithInput); -static_assert(SomeHandlerWithTypedInput); -static_assert(SomeHandlerWithInput); -static_assert(not SomeHandlerWithTypedInput); - -// The four tests below exercise the typed path — a handler whose spec, validation and -// deserialization all come from the shared consteval spec via HandlerFor. They run -// against the same DefaultProcessor as the legacy tests above, which is the point: the -// dual path is a dispatch detail, not a second processor. - -TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HappyPath) +TEST_F(RPCDefaultProcessorTest, SpecHandler_HappyPath) { runSpawn([](auto yield) { TypedHandlerFake const handler; @@ -96,7 +55,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HappyPath) }); } -TEST_F(RPCDefaultProcessorTest, NewSpecHandler_MissingRequiredField_ReturnsError) +TEST_F(RPCDefaultProcessorTest, SpecHandler_MissingRequiredField_ReturnsError) { runSpawn([](auto yield) { TypedHandlerFake const handler; @@ -110,7 +69,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_MissingRequiredField_ReturnsError }); } -TEST_F(RPCDefaultProcessorTest, NewSpecHandler_DeprecatedField_WarningsForwarded) +TEST_F(RPCDefaultProcessorTest, SpecHandler_DeprecatedField_WarningsForwarded) { runSpawn([](auto yield) { TypedHandlerFake const handler; @@ -124,7 +83,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_DeprecatedField_WarningsForwarded }); } -TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HandlerReturnsError_ForwardsError) +TEST_F(RPCDefaultProcessorTest, SpecHandler_HandlerReturnsError_ForwardsError) { runSpawn([](auto yield) { FailingTypedHandlerFake const handler; @@ -139,7 +98,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HandlerReturnsError_ForwardsError }); } -TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HandlerReturnsError_StillForwardsWarnings) +TEST_F(RPCDefaultProcessorTest, SpecHandler_HandlerReturnsError_StillForwardsWarnings) { runSpawn([](auto yield) { FailingTypedHandlerFake const handler; @@ -154,7 +113,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HandlerReturnsError_StillForwards }); } -TEST_F(RPCDefaultProcessorTest, NewSpecHandler_DeprecatedFieldAbsent_NoWarnings) +TEST_F(RPCDefaultProcessorTest, SpecHandler_DeprecatedFieldAbsent_NoWarnings) { runSpawn([](auto yield) { TypedHandlerFake const handler; diff --git a/tests/unit/rpc/handlers/LedgerEntryTests.cpp b/tests/unit/rpc/handlers/LedgerEntryTests.cpp index 232396d4c..ee957e5a1 100644 --- a/tests/unit/rpc/handlers/LedgerEntryTests.cpp +++ b/tests/unit/rpc/handlers/LedgerEntryTests.cpp @@ -246,7 +246,8 @@ generateTestValuesForParametersTest() kAccount ), .expectedError = "malformedRequest", - .expectedErrorMessage = "Must have one of authorized or authorized_credentials." + .expectedErrorMessage = + "Must have exactly one of `authorized` and `authorized_credentials`." }, ParamTestCaseBundle{ @@ -270,7 +271,8 @@ generateTestValuesForParametersTest() kCredentialType ), .expectedError = "malformedRequest", - .expectedErrorMessage = "Must have one of authorized or authorized_credentials." + .expectedErrorMessage = + "Must have exactly one of `authorized` and `authorized_credentials`." }, ParamTestCaseBundle{ @@ -805,7 +807,7 @@ generateTestValuesForParametersTest() "directory": {} })JSON", .expectedError = "invalidParams", - .expectedErrorMessage = "missingOwnerOrDirRoot" + .expectedErrorMessage = "Must have exactly one of `owner` and `dir_root` fields." }, ParamTestCaseBundle{ @@ -865,7 +867,7 @@ generateTestValuesForParametersTest() kAccount ), .expectedError = "invalidParams", - .expectedErrorMessage = "mayNotSpecifyBothDirRootAndOwner" + .expectedErrorMessage = "Must have exactly one of `owner` and `dir_root` fields." }, ParamTestCaseBundle{ diff --git a/tests/unit/rpc/handlers/NFTInfoTests.cpp b/tests/unit/rpc/handlers/NFTInfoTests.cpp index 76f0d719a..6f0069dd7 100644 --- a/tests/unit/rpc/handlers/NFTInfoTests.cpp +++ b/tests/unit/rpc/handlers/NFTInfoTests.cpp @@ -59,6 +59,53 @@ TEST_F(RPCNFTInfoHandlerTest, NonHexLedgerHash) }); } +// nft_info is Clio-only, so ForwardingProxy::shouldForward returns false before it gets to the +// current/closed check and the request is dispatched here rather than sent to xrpld. Clio holds +// neither ledger, so the resolver has to reject them itself. +TEST_F(RPCNFTInfoHandlerTest, CurrentLedgerIndexRejected) +{ + runSpawn([this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{NFTInfoHandler{backend_}}; + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "nft_id": "{}", + "ledger_index": "current" + }})JSON", + kNftId + ) + ); + auto const output = handler.process(input, Context{.yield = yield}); + ASSERT_FALSE(output); + + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerIndexMalformed"); + }); +} + +TEST_F(RPCNFTInfoHandlerTest, ClosedLedgerIndexRejected) +{ + runSpawn([this](boost::asio::yield_context yield) { + auto const handler = AnyHandler{NFTInfoHandler{backend_}}; + auto const input = boost::json::parse( + fmt::format( + R"JSON({{ + "nft_id": "{}", + "ledger_index": "closed" + }})JSON", + kNftId + ) + ); + auto const output = handler.process(input, Context{.yield = yield}); + ASSERT_FALSE(output); + + auto const err = rpc::makeError(output.result.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerIndexMalformed"); + }); +} + TEST_F(RPCNFTInfoHandlerTest, NonStringLedgerHash) { runSpawn([this](boost::asio::yield_context yield) { diff --git a/tests/unit/rpc/handlers/TestHandlerTests.cpp b/tests/unit/rpc/handlers/TestHandlerTests.cpp index 84a07769f..3d3c9bd9c 100644 --- a/tests/unit/rpc/handlers/TestHandlerTests.cpp +++ b/tests/unit/rpc/handlers/TestHandlerTests.cpp @@ -10,7 +10,6 @@ using namespace std; using namespace rpc; -using namespace rpc::validation; using namespace tests::common; class RPCTestHandlerTest : public HandlerBaseTest {}; @@ -19,7 +18,7 @@ class RPCTestHandlerTest : public HandlerBaseTest {}; TEST_F(RPCTestHandlerTest, HandlerSuccess) { runSpawn([](auto yield) { - auto const handler = AnyHandler{HandlerFake{}}; + auto const handler = AnyHandler{TypedHandlerFake{}}; auto const input = boost::json::parse(R"JSON({ "hello": "world", "limit": 10 @@ -48,7 +47,7 @@ TEST_F(RPCTestHandlerTest, NoInputHandlerSuccess) TEST_F(RPCTestHandlerTest, HandlerErrorHandling) { runSpawn([](auto yield) { - auto const handler = AnyHandler{HandlerFake{}}; + auto const handler = AnyHandler{TypedHandlerFake{}}; auto const input = boost::json::parse(R"JSON({ "hello": "not world", "limit": 10 @@ -67,7 +66,7 @@ TEST_F(RPCTestHandlerTest, HandlerErrorHandling) TEST_F(RPCTestHandlerTest, HandlerInnerErrorHandling) { runSpawn([](auto yield) { - auto const handler = AnyHandler{FailingHandlerFake{}}; + auto const handler = AnyHandler{FailingTypedHandlerFake{}}; auto const input = boost::json::parse(R"JSON({ "hello": "world", "limit": 10 diff --git a/tests/unit/rpc/handlers/VaultInfoTests.cpp b/tests/unit/rpc/handlers/VaultInfoTests.cpp index 782f2129c..01e063531 100644 --- a/tests/unit/rpc/handlers/VaultInfoTests.cpp +++ b/tests/unit/rpc/handlers/VaultInfoTests.cpp @@ -70,27 +70,27 @@ generateTestValuesForParametersTest() .testJson = R"JSON({ "idk": "idk" })JSON", - .expectedError = "malformedRequest", - .expectedErrorCode = ClioError::RpcMalformedRequest, - .expectedErrorMessage = "Malformed request." + .expectedError = "invalidParams", + .expectedErrorCode = RippledError::RpcInvalidParams, + .expectedErrorMessage = "Must specify either 'vault_id' or both 'owner' and 'seq'." }, VaultInfoParamTestCaseBundle{ .testName = "MissingOwnerInVault", .testJson = R"JSON({ "seq": 4 })JSON", - .expectedError = "malformedRequest", - .expectedErrorCode = ClioError::RpcMalformedRequest, - .expectedErrorMessage = "Malformed request." + .expectedError = "invalidParams", + .expectedErrorCode = RippledError::RpcInvalidParams, + .expectedErrorMessage = "Must specify either 'vault_id' or both 'owner' and 'seq'." }, VaultInfoParamTestCaseBundle{ .testName = "MissingSeqInVault", .testJson = R"JSON({ "owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" })JSON", - .expectedError = "malformedRequest", - .expectedErrorCode = ClioError::RpcMalformedRequest, - .expectedErrorMessage = "Malformed request." + .expectedError = "invalidParams", + .expectedErrorCode = RippledError::RpcInvalidParams, + .expectedErrorMessage = "Must specify either 'vault_id' or both 'owner' and 'seq'." }, VaultInfoParamTestCaseBundle{ .testName = "SeqNotAnInteger", @@ -150,9 +150,9 @@ generateTestValuesForParametersTest() kVaultId, kAccount ), - .expectedError = "malformedRequest", - .expectedErrorCode = ClioError::RpcMalformedRequest, - .expectedErrorMessage = "Malformed request." + .expectedError = "invalidParams", + .expectedErrorCode = RippledError::RpcInvalidParams, + .expectedErrorMessage = "Must specify either 'vault_id' or both 'owner' and 'seq'." } }; }