Files
clio/tests/unit/rpc/BaseTests.cpp
2026-09-15 22:43:39 +01:00

839 lines
29 KiB
C++

#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 <boost/json/array.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/value.hpp>
#include <fmt/format.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <rpcspec/Errors.hpp>
#include <xrpl/protocol/ErrorCodes.h>
#include <cstdint>
#include <limits>
#include <string>
#include <string_view>
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<string>(jString));
ASSERT_FALSE(checkType<int>(jString));
}
TEST_F(RPCBaseTest, CheckTypeUint)
{
auto const jUint = boost::json::value(123u);
ASSERT_TRUE(checkType<uint32_t>(jUint));
ASSERT_TRUE(checkType<int32_t>(jUint));
ASSERT_FALSE(checkType<bool>(jUint));
}
TEST_F(RPCBaseTest, CheckTypeInt)
{
auto jInt = boost::json::value(123);
ASSERT_TRUE(checkType<int32_t>(jInt));
ASSERT_TRUE(checkType<uint32_t>(jInt));
ASSERT_FALSE(checkType<bool>(jInt));
jInt = boost::json::value(-123);
ASSERT_TRUE(checkType<int32_t>(jInt));
ASSERT_FALSE(checkType<uint32_t>(jInt)); // Unsigned can't be negative
ASSERT_FALSE(checkType<bool>(jInt));
}
TEST_F(RPCBaseTest, CheckTypeBool)
{
auto const jBool = boost::json::value(true);
ASSERT_TRUE(checkType<bool>(jBool));
ASSERT_FALSE(checkType<int>(jBool));
}
TEST_F(RPCBaseTest, CheckTypeDouble)
{
auto const jDouble = boost::json::value(0.123);
ASSERT_TRUE(checkType<double>(jDouble));
ASSERT_TRUE(checkType<float>(jDouble));
ASSERT_FALSE(checkType<bool>(jDouble));
}
TEST_F(RPCBaseTest, CheckTypeArray)
{
auto const jArr = boost::json::value({1, 2, 3});
ASSERT_TRUE(checkType<boost::json::array>(jArr));
ASSERT_FALSE(checkType<int>(jArr));
}
TEST_F(RPCBaseTest, CheckTypeAndClampValueUnchanged)
{
auto jUint = boost::json::value(123u);
ASSERT_TRUE(checkTypeAndClamp<uint32_t>(jUint));
ASSERT_EQ(jUint.as_uint64(), 123u);
ASSERT_TRUE(checkTypeAndClamp<int32_t>(jUint));
ASSERT_EQ(jUint.as_uint64(), 123u);
auto jInt = boost::json::value(123);
ASSERT_TRUE(checkTypeAndClamp<int32_t>(jInt));
ASSERT_EQ(jInt.as_int64(), 123);
ASSERT_TRUE(checkTypeAndClamp<uint32_t>(jInt));
ASSERT_EQ(jInt.as_int64(), 123);
jInt = boost::json::value(-123);
ASSERT_TRUE(checkTypeAndClamp<int32_t>(jInt));
ASSERT_EQ(jInt.as_int64(), -123);
}
TEST_F(RPCBaseTest, CheckTypeAndClampInvalidValues)
{
auto jInt = boost::json::value(-123);
ASSERT_FALSE(checkTypeAndClamp<uint32_t>(jInt)); // Unsigned can't be negative
}
TEST_F(RPCBaseTest, CheckTypeAndClampOverflow)
{
auto jBigUint = boost::json::value(std::numeric_limits<uint64_t>::max());
ASSERT_TRUE(checkTypeAndClamp<uint32_t>(jBigUint));
ASSERT_EQ(jBigUint.as_uint64(), std::numeric_limits<uint32_t>::max());
auto jBigInt = boost::json::value(std::numeric_limits<int64_t>::max());
ASSERT_TRUE(checkTypeAndClamp<int32_t>(jBigInt));
ASSERT_EQ(jBigInt.as_int64(), std::numeric_limits<int32_t>::max());
}
TEST_F(RPCBaseTest, CheckTypeAndClampUnderflow)
{
auto jLowInt = boost::json::value(std::numeric_limits<int64_t>::min());
ASSERT_TRUE(checkTypeAndClamp<int32_t>(jLowInt));
ASSERT_EQ(jLowInt.as_int64(), std::numeric_limits<int32_t>::min());
}
TEST_F(RPCBaseTest, TypeValidator)
{
auto spec = RpcSpec{
{"uint", Type<uint32_t>{}},
{"int", Type<int32_t>{}},
{"str", Type<string>{}},
{"double", Type<double>{}},
{"bool", Type<bool>{}},
{"arr", Type<boost::json::array>{}},
};
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<uint32_t, string>{}},
};
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<uint32_t>{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<std::uint32_t>{}, Min{std::numeric_limits<uint32_t>::max()}},
{"amount2", Type<std::int32_t>{}, Min{std::numeric_limits<int32_t>::max()}},
{"amount3", Type<std::int32_t>{}, Min{std::numeric_limits<int32_t>::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<std::uint32_t>{}, Max{std::numeric_limits<uint32_t>::max()}},
{"amount2", Type<std::int32_t>{}, Max{std::numeric_limits<int32_t>::max()}},
{"amount3", Type<std::int32_t>{}, Max{std::numeric_limits<int32_t>::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<boost::json::array>{},
ValidateArrayAt{
0,
{
{"limit", Required{}, Type<uint32_t>{}, Between<uint32_t>{0, 100}},
}
}},
{"arr2",
ValidateArrayAt{
0,
{
{"limit", Required{}, Type<uint32_t>{}, Between<uint32_t>{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<std::string, boost::json::object>{},
IfType<boost::json::object>{
Section{{"limit", Required{}, Type<uint32_t>{}, Between<uint32_t>{0, 100}}},
Section{{"limit2", Required{}, Type<uint32_t>{}, Between<uint32_t>{0, 100}}}
},
IfType<std::string>{CustomValidators::uint256HexStringValidator}},
{"mix2",
Section{{"limit", Required{}, Type<uint32_t>{}, Between<uint32_t>{0, 100}}},
Type<std::string, boost::json::object>{}},
};
// 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<std::string>{}, 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<uint32_t>{}, 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 '<key>.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<uint32_t>{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<MaybeError(boost::json::value & value, std::string_view)>>
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));
}