test: Add RPC error checking support to unit tests (#4987)

This commit is contained in:
Ed Hennis
2024-04-24 13:54:46 -04:00
committed by GitHub
parent b84f7e7c10
commit e9859ac1b1
10 changed files with 230 additions and 45 deletions

View File

@@ -250,7 +250,8 @@ public:
env(noop(alice),
msig(demon, demon),
fee(3 * baseFee),
ter(telENV_RPC_FAILED));
rpc("invalidTransaction",
"fails local checks: Duplicate Signers not allowed."));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
@@ -361,7 +362,10 @@ public:
msig phantoms{bogie, demon};
std::reverse(phantoms.signers.begin(), phantoms.signers.end());
std::uint32_t const aliceSeq = env.seq(alice);
env(noop(alice), phantoms, ter(telENV_RPC_FAILED));
env(noop(alice),
phantoms,
rpc("invalidTransaction",
"fails local checks: Unsorted Signers array."));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
}
@@ -1640,7 +1644,8 @@ public:
env(noop(alice),
msig(demon, demon),
fee(3 * baseFee),
ter(telENV_RPC_FAILED));
rpc("invalidTransaction",
"fails local checks: Duplicate Signers not allowed."));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);

View File

@@ -149,7 +149,9 @@ struct Regression_test : public beast::unit_test::suite
secp256r1Sig->setFieldVL(sfSigningPubKey, *pubKeyBlob);
jt.stx.reset(secp256r1Sig.release());
env(jt, ter(telENV_RPC_FAILED));
env(jt,
rpc("invalidTransaction",
"fails local checks: Invalid signature."));
};
Account const alice{"alice", KeyType::secp256k1};

View File

@@ -1058,16 +1058,17 @@ public:
auto const& jt = env.jt(noop(alice));
BEAST_EXPECT(jt.stx);
bool didApply;
TER ter;
Env::ParsedResult parsed;
env.app().openLedger().modify(
[&](OpenView& view, beast::Journal j) {
std::tie(ter, didApply) = ripple::apply(
// No need to initialize, since it's about to get set
bool didApply;
std::tie(parsed.ter, didApply) = ripple::apply(
env.app(), view, *jt.stx, tapNONE, env.journal);
return didApply;
});
env.postconditions(jt, ter, didApply);
env.postconditions(jt, parsed);
}
checkMetrics(__LINE__, env, 1, std::nullopt, 4, 2, 256);

View File

@@ -54,6 +54,7 @@
#include <test/jtx/regkey.h>
#include <test/jtx/require.h>
#include <test/jtx/requires.h>
#include <test/jtx/rpc.h>
#include <test/jtx/sendmax.h>
#include <test/jtx/seq.h>
#include <test/jtx/sig.h>

View File

@@ -120,6 +120,20 @@ public:
Account const& master = Account::master;
/// Used by parseResult() and postConditions()
struct ParsedResult
{
std::optional<TER> ter{};
// RPC errors tend to return either a "code" and a "message" (sometimes
// with an "error" that corresponds to the "code"), or with an "error"
// and an "exception". However, this structure allows all possible
// combinations.
std::optional<error_code_i> rpcCode{};
std::string rpcMessage;
std::string rpcError;
std::string rpcException;
};
private:
struct AppBundle
{
@@ -493,7 +507,7 @@ public:
/** Gets the TER result and `didApply` flag from a RPC Json result object.
*/
static std::pair<TER, bool>
static ParsedResult
parseResult(Json::Value const& jr);
/** Submit an existing JTx.
@@ -514,8 +528,7 @@ public:
void
postconditions(
JTx const& jt,
TER ter,
bool didApply,
ParsedResult const& parsed,
Json::Value const& jr = Json::Value());
/** Apply funclets and submit. */

View File

@@ -747,9 +747,12 @@ public:
// Force the factor low enough to fail
params[jss::fee_mult_max] = 1;
params[jss::fee_div_max] = 2;
// RPC errors result in telENV_RPC_FAILED
envs(noop(alice), fee(none), seq(none), ter(telENV_RPC_FAILED))(
params);
envs(
noop(alice),
fee(none),
seq(none),
rpc(rpcHIGH_FEE,
"Fee of 10 exceeds the requested tx limit of 5"))(params);
auto tx = env.tx();
BEAST_EXPECT(!tx);

View File

@@ -44,6 +44,9 @@ struct JTx
Json::Value jv;
requires_t require;
std::optional<TER> ter = TER{tesSUCCESS};
std::optional<std::pair<error_code_i, std::string>> rpcCode = std::nullopt;
std::optional<std::pair<std::string, std::optional<std::string>>>
rpcException = std::nullopt;
bool fill_fee = true;
bool fill_seq = true;
bool fill_sig = true;

View File

@@ -272,24 +272,48 @@ Env::trust(STAmount const& amount, Account const& account)
test.expect(balance(account) == start);
}
std::pair<TER, bool>
Env::ParsedResult
Env::parseResult(Json::Value const& jr)
{
TER ter;
if (jr.isObject() && jr.isMember(jss::result) &&
jr[jss::result].isMember(jss::engine_result_code))
ter = TER::fromInt(jr[jss::result][jss::engine_result_code].asInt());
auto error = [](ParsedResult& parsed, Json::Value const& object) {
// Use an error code that is not used anywhere in the transaction
// engine to distinguish this case.
parsed.ter = telENV_RPC_FAILED;
// Extract information about the error
if (!object.isObject())
return;
if (object.isMember(jss::error_code))
parsed.rpcCode =
safe_cast<error_code_i>(object[jss::error_code].asInt());
if (object.isMember(jss::error_message))
parsed.rpcMessage = object[jss::error_message].asString();
if (object.isMember(jss::error))
parsed.rpcError = object[jss::error].asString();
if (object.isMember(jss::error_exception))
parsed.rpcException = object[jss::error_exception].asString();
};
ParsedResult parsed;
if (jr.isObject() && jr.isMember(jss::result))
{
auto const& result = jr[jss::result];
if (result.isMember(jss::engine_result_code))
{
parsed.ter = TER::fromInt(result[jss::engine_result_code].asInt());
parsed.rpcCode.emplace(rpcSUCCESS);
}
else
error(parsed, result);
}
else
// Use an error code that is not used anywhere in the transaction engine
// to distinguish this case.
ter = telENV_RPC_FAILED;
return std::make_pair(ter, isTesSuccess(ter) || isTecClaim(ter));
error(parsed, jr);
return parsed;
}
void
Env::submit(JTx const& jt)
{
bool didApply;
ParsedResult parsedResult;
auto const jr = [&]() {
if (jt.stx)
{
@@ -298,7 +322,9 @@ Env::submit(JTx const& jt)
jt.stx->add(s);
auto const jr = rpc("submit", strHex(s.slice()));
std::tie(ter_, didApply) = parseResult(jr);
parsedResult = parseResult(jr);
test.expect(parsedResult.ter, "ter uninitialized!");
ter_ = parsedResult.ter.value_or(telENV_RPC_FAILED);
return jr;
}
@@ -306,20 +332,17 @@ Env::submit(JTx const& jt)
{
// Parsing failed or the JTx is
// otherwise missing the stx field.
ter_ = temMALFORMED;
didApply = false;
parsedResult.ter = ter_ = temMALFORMED;
return Json::Value();
}
}();
return postconditions(jt, ter_, didApply, jr);
return postconditions(jt, parsedResult, jr);
}
void
Env::sign_and_submit(JTx const& jt, Json::Value params)
{
bool didApply;
auto const account = lookup(jt.jv[jss::Account].asString());
auto const& passphrase = account.name();
@@ -348,24 +371,55 @@ Env::sign_and_submit(JTx const& jt, Json::Value params)
if (!txid_.parseHex(jr[jss::result][jss::tx_json][jss::hash].asString()))
txid_.zero();
std::tie(ter_, didApply) = parseResult(jr);
ParsedResult const parsedResult = parseResult(jr);
test.expect(parsedResult.ter, "ter uninitialized!");
ter_ = parsedResult.ter.value_or(telENV_RPC_FAILED);
return postconditions(jt, ter_, didApply, jr);
return postconditions(jt, parsedResult, jr);
}
void
Env::postconditions(
JTx const& jt,
TER ter,
bool didApply,
ParsedResult const& parsed,
Json::Value const& jr)
{
if (jt.ter &&
!test.expect(
ter == *jt.ter,
"apply: Got " + transToken(ter) + " (" + transHuman(ter) +
"); Expected " + transToken(*jt.ter) + " (" +
transHuman(*jt.ter) + ")"))
bool bad = !test.expect(parsed.ter, "apply: No ter result!");
bad =
(jt.ter && parsed.ter &&
!test.expect(
*parsed.ter == *jt.ter,
"apply: Got " + transToken(*parsed.ter) + " (" +
transHuman(*parsed.ter) + "); Expected " +
transToken(*jt.ter) + " (" + transHuman(*jt.ter) + ")"));
using namespace std::string_literals;
bad = (jt.rpcCode &&
!test.expect(
parsed.rpcCode == jt.rpcCode->first &&
parsed.rpcMessage == jt.rpcCode->second,
"apply: Got RPC result "s +
(parsed.rpcCode
? RPC::get_error_info(*parsed.rpcCode).token.c_str()
: "NO RESULT") +
" (" + parsed.rpcMessage + "); Expected " +
RPC::get_error_info(jt.rpcCode->first).token.c_str() + " (" +
jt.rpcCode->second + ")")) ||
bad;
// If we have an rpcCode (just checked), then the rpcException check is
// optional - the 'error' field may not be defined, but if it is, it must
// match rpcError.
bad =
(jt.rpcException &&
!test.expect(
(jt.rpcCode && parsed.rpcError.empty()) ||
(parsed.rpcError == jt.rpcException->first &&
(!jt.rpcException->second ||
parsed.rpcException == *jt.rpcException->second)),
"apply: Got RPC result "s + parsed.rpcError + " (" +
parsed.rpcException + "); Expected " + jt.rpcException->first +
" (" + jt.rpcException->second.value_or("n/a") + ")")) ||
bad;
if (bad)
{
test.log << pretty(jt.jv) << std::endl;
if (jr)

86
src/test/jtx/rpc.h Normal file
View File

@@ -0,0 +1,86 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TEST_JTX_RPC_H_INCLUDED
#define RIPPLE_TEST_JTX_RPC_H_INCLUDED
#include <test/jtx/Env.h>
#include <tuple>
namespace ripple {
namespace test {
namespace jtx {
/** Set the expected result code for a JTx
The test will fail if the code doesn't match.
*/
class rpc
{
private:
std::optional<error_code_i> code_;
std::optional<std::string> errorMessage_;
std::optional<std::string> error_;
std::optional<std::string> errorException_;
public:
/// If there's an error code, we expect an error message
explicit rpc(error_code_i code, std::optional<std::string> m = {})
: code_(code), errorMessage_(m)
{
}
/// If there is not a code, we expect an exception message
explicit rpc(
std::string error,
std::optional<std::string> exceptionMessage = {})
: error_(error), errorException_(exceptionMessage)
{
}
void
operator()(Env&, JTx& jt) const
{
// The RPC request should fail. RPC errors result in telENV_RPC_FAILED.
jt.ter = telENV_RPC_FAILED;
if (code_)
{
auto const& errorInfo = RPC::get_error_info(*code_);
// When an RPC request returns an error code ('error_code'), it
// always includes an error message ('error_message'), and sometimes
// includes an error token ('error'). If it does, the error token is
// always obtained from the lookup into the ErrorInfo lookup table.
//
// Take advantage of that fact to populate jt.rpcException. The
// check will be aware of whether the rpcExcpetion can be safely
// ignored.
jt.rpcCode = {
*code_,
errorMessage_ ? *errorMessage_ : errorInfo.message.c_str()};
jt.rpcException = {errorInfo.token.c_str(), std::nullopt};
}
if (error_)
jt.rpcException = {*error_, errorException_};
}
};
} // namespace jtx
} // namespace test
} // namespace ripple
#endif

View File

@@ -56,7 +56,10 @@ public:
JTx memoSize = makeJtxWithMemo();
memoSize.jv[sfMemos.jsonName][0u][sfMemo.jsonName]
[sfMemoData.jsonName] = std::string(2020, '0');
env(memoSize, ter(telENV_RPC_FAILED));
env(memoSize,
rpc("invalidTransaction",
"fails local checks: The memo exceeds the maximum allowed "
"size."));
// This memo is just barely small enough.
memoSize.jv[sfMemos.jsonName][0u][sfMemo.jsonName]
@@ -72,7 +75,10 @@ public:
auto& m = mi[sfCreatedNode.jsonName]; // CreatedNode in Memos
m[sfMemoData.jsonName] = "3030303030";
env(memoNonMemo, ter(telENV_RPC_FAILED));
env(memoNonMemo,
rpc("invalidTransaction",
"fails local checks: A memo array may contain only Memo "
"objects."));
}
{
// Put an invalid field in a Memo object.
@@ -80,7 +86,10 @@ public:
memoExtra
.jv[sfMemos.jsonName][0u][sfMemo.jsonName][sfFlags.jsonName] =
13;
env(memoExtra, ter(telENV_RPC_FAILED));
env(memoExtra,
rpc("invalidTransaction",
"fails local checks: A memo may contain only MemoType, "
"MemoData or MemoFormat fields."));
}
{
// Put a character that is not allowed in a URL in a MemoType field.
@@ -88,7 +97,11 @@ public:
memoBadChar.jv[sfMemos.jsonName][0u][sfMemo.jsonName]
[sfMemoType.jsonName] =
strHex(std::string_view("ONE<INFINITY"));
env(memoBadChar, ter(telENV_RPC_FAILED));
env(memoBadChar,
rpc("invalidTransaction",
"fails local checks: The MemoType and MemoFormat fields "
"may only contain characters that are allowed in URLs "
"under RFC 3986."));
}
{
// Put a character that is not allowed in a URL in a MemoData field.
@@ -105,7 +118,11 @@ public:
memoBadChar.jv[sfMemos.jsonName][0u][sfMemo.jsonName]
[sfMemoFormat.jsonName] =
strHex(std::string_view("NoBraces{}InURL"));
env(memoBadChar, ter(telENV_RPC_FAILED));
env(memoBadChar,
rpc("invalidTransaction",
"fails local checks: The MemoType and MemoFormat fields "
"may only contain characters that are allowed in URLs "
"under RFC 3986."));
}
}