Add Ticket Tests (RIPD-1328):

Migrate ticket-test.js to cpp. Add coverage for Change/Cancel ticket
handlers.
This commit is contained in:
Mike Ellery
2016-11-01 08:09:27 -07:00
committed by Nik Bougalis
parent 35504f1723
commit 7e39d645b9
6 changed files with 423 additions and 2 deletions

View File

@@ -4402,6 +4402,10 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\test\app\Ticket_test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\test\app\Transaction_ordering_test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>

View File

@@ -5205,6 +5205,9 @@
<ClCompile Include="..\..\src\test\app\Taker_test.cpp">
<Filter>test\app</Filter>
</ClCompile>
<ClCompile Include="..\..\src\test\app\Ticket_test.cpp">
<Filter>test\app</Filter>
</ClCompile>
<ClCompile Include="..\..\src\test\app\Transaction_ordering_test.cpp">
<Filter>test\app</Filter>
</ClCompile>

View File

@@ -46,6 +46,16 @@ create (Account const& account,
} // detail
Json::Value
cancel(Account const& account, std::string const & ticketId)
{
Json::Value jv;
jv[jss::TransactionType] = "TicketCancel";
jv[jss::Account] = account.human();
jv["TicketID"] = ticketId;
return jv;
}
} // ticket
} // jtx

View File

@@ -97,8 +97,8 @@ create (Account const& account,
}
/** Cancel a ticket */
//Json::Value
//cancel (Account const& account,
Json::Value
cancel(Account const& account, std::string const & ticketId);
} // ticket

View File

@@ -0,0 +1,403 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2016 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.
*/
//==============================================================================
#include <BeastConfig.h>
#include <ripple/test/jtx.h>
#include <ripple/protocol/JsonFields.h>
#include <ripple/protocol/Feature.h>
namespace ripple {
class Ticket_test : public beast::unit_test::suite
{
static auto constexpr idOne =
"00000000000000000000000000000000"
"00000000000000000000000000000001";
/// @brief validate metadata for a create/cancel ticket transaction and
/// return the 3 or 4 nodes that make-up the metadata (AffectedNodes)
///
/// @param env current jtx env (meta will be extracted from it)
///
/// @param other_target flag to indicate whether a Target different
/// from the Account was specified for the ticket (when created)
///
/// @param expiration flag to indicate a cancellation with expiration which
/// causes two of the affected nodes to be swapped (in order).
///
/// @retval std::array size 4 of json object values representing
/// each meta node entry. When the transaction was a cancel with differing
/// target and account, there will be 4 complete items, otherwise the last
/// entry will be an empty object
auto
checkTicketMeta(
test::jtx::Env& env,
bool other_target = false,
bool expiration = false)
{
using namespace std::string_literals;
auto const& tx = env.tx ()->getJson (0);
bool is_cancel = tx[jss::TransactionType] == "TicketCancel";
auto const& jvm = env.meta ()->getJson (0);
std::array<Json::Value, 4> retval;
// these are the affected nodes that we expect for
// a few different scenarios.
// tuple is index, field name, and label (LedgerEntryType)
std::vector<
std::tuple<std::size_t, std::string, std::string>
> expected_nodes;
if (is_cancel && other_target)
{
expected_nodes = {
std::make_tuple(0, sfModifiedNode.fieldName, "AccountRoot"s),
std::make_tuple(
expiration ? 2: 1, sfModifiedNode.fieldName, "AccountRoot"s),
std::make_tuple(
expiration ? 1: 2, sfDeletedNode.fieldName, "Ticket"s),
std::make_tuple(3, sfDeletedNode.fieldName, "DirectoryNode"s)
};
}
else
{
expected_nodes = {
std::make_tuple(0, sfModifiedNode.fieldName, "AccountRoot"s),
std::make_tuple(1,
is_cancel ?
sfDeletedNode.fieldName : sfCreatedNode.fieldName,
"Ticket"s),
std::make_tuple(2,
is_cancel ?
sfDeletedNode.fieldName : sfCreatedNode.fieldName,
"DirectoryNode"s)
};
}
BEAST_EXPECT(jvm.isMember (sfAffectedNodes.fieldName));
BEAST_EXPECT(jvm[sfAffectedNodes.fieldName].isArray());
BEAST_EXPECT(
jvm[sfAffectedNodes.fieldName].size() == expected_nodes.size());
// verify the actual metadata against the expected
for (auto const& it : expected_nodes)
{
auto const& idx = std::get<0>(it);
auto const& field = std::get<1>(it);
auto const& type = std::get<2>(it);
BEAST_EXPECT(jvm[sfAffectedNodes.fieldName][idx].isMember(field));
retval[idx] = jvm[sfAffectedNodes.fieldName][idx][field];
BEAST_EXPECT(retval[idx][sfLedgerEntryType.fieldName] == type);
}
return retval;
}
void testTicketNotEnabled ()
{
testcase ("Feature Not Enabled");
using namespace test::jtx;
Env env {*this};
env (ticket::create (env.master), ter(temDISABLED));
env (ticket::cancel (env.master, idOne), ter (temDISABLED));
}
void testTicketCancelNonexistent ()
{
testcase ("Cancel Nonexistent");
using namespace test::jtx;
Env env {*this, features (featureTickets)};
env (ticket::cancel (env.master, idOne), ter (tecNO_ENTRY));
}
void testTicketCreatePreflightFail ()
{
testcase ("Create/Cancel Ticket with Bad Fee, Fail Preflight");
using namespace test::jtx;
Env env {*this, features (featureTickets)};
env (ticket::create (env.master), fee (XRP (-1)), ter (temBAD_FEE));
env (ticket::cancel (env.master, idOne), fee (XRP (-1)), ter (temBAD_FEE));
}
void testTicketCreateNonexistent ()
{
testcase ("Create Tickets with Nonexistent Accounts");
using namespace test::jtx;
Env env {*this, features (featureTickets)};
Account alice {"alice"};
env.memoize (alice);
env (ticket::create (env.master, alice), ter(tecNO_TARGET));
env (ticket::create (alice, env.master),
json (jss::Sequence, 1),
ter (terNO_ACCOUNT));
}
void testTicketToSelf ()
{
testcase ("Create Tickets with Same Account and Target");
using namespace test::jtx;
Env env {*this, features (featureTickets)};
env (ticket::create (env.master, env.master));
auto cr = checkTicketMeta (env);
auto const& jticket = cr[1];
BEAST_EXPECT(jticket[sfLedgerIndex.fieldName] ==
"7F58A0AE17775BA3404D55D406DD1C2E91EADD7AF3F03A26877BCE764CCB75E3");
BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Account] ==
env.master.human());
BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Sequence] == 1);
//verify that there's no `Target` saved in the ticket
BEAST_EXPECT(! jticket[sfNewFields.fieldName].
isMember(sfTarget.fieldName));
}
void testTicketCancelByCreator ()
{
testcase ("Create Ticket and Then Cancel by Creator");
using namespace test::jtx;
Env env {*this, features (featureTickets)};
// create and verify
env (ticket::create (env.master));
auto cr = checkTicketMeta (env);
auto const& jacct = cr[0];
auto const& jticket = cr[1];
BEAST_EXPECT(
jacct[sfPreviousFields.fieldName][sfOwnerCount.fieldName] == 0);
BEAST_EXPECT(
jacct[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 1);
BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Sequence] ==
jacct[sfPreviousFields.fieldName][jss::Sequence]);
BEAST_EXPECT(jticket[sfLedgerIndex.fieldName] ==
"7F58A0AE17775BA3404D55D406DD1C2E91EADD7AF3F03A26877BCE764CCB75E3");
BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Account] ==
env.master.human());
// cancel
env (ticket::cancel(env.master, jticket[sfLedgerIndex.fieldName].asString()));
auto crd = checkTicketMeta (env);
auto const& jacctd = crd[0];
BEAST_EXPECT(jacctd[sfFinalFields.fieldName][jss::Sequence] == 3);
BEAST_EXPECT(
jacctd[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 0);
}
void testTicketInsufficientReserve ()
{
testcase ("Create Ticket Insufficient Reserve");
using namespace test::jtx;
Env env {*this, features (featureTickets)};
Account alice {"alice"};
env.fund (env.current ()->fees ().accountReserve (0), alice);
env.close ();
env (ticket::create (alice), ter (tecINSUFFICIENT_RESERVE));
}
void testTicketCancelByTarget ()
{
testcase ("Create Ticket and Then Cancel by Target");
using namespace test::jtx;
Env env {*this, features (featureTickets)};
Account alice {"alice"};
env.fund (XRP (10000), alice);
env.close ();
// create and verify
env (ticket::create (env.master, alice));
auto cr = checkTicketMeta (env, true);
auto const& jacct = cr[0];
auto const& jticket = cr[1];
BEAST_EXPECT(
jacct[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 1);
BEAST_EXPECT(jticket[sfLedgerEntryType.fieldName] == "Ticket");
BEAST_EXPECT(jticket[sfLedgerIndex.fieldName] ==
"C231BA31A0E13A4D524A75F990CE0D6890B800FF1AE75E51A2D33559547AC1A2");
BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Account] ==
env.master.human());
BEAST_EXPECT(jticket[sfNewFields.fieldName][sfTarget.fieldName] ==
alice.human());
BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Sequence] == 2);
// cancel using the target account
env (ticket::cancel(alice, jticket[sfLedgerIndex.fieldName].asString()));
auto crd = checkTicketMeta (env, true);
auto const& jacctd = crd[0];
auto const& jdir = crd[2];
BEAST_EXPECT(
jacctd[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 0);
BEAST_EXPECT(jdir[sfLedgerIndex.fieldName] ==
jticket[sfLedgerIndex.fieldName]);
BEAST_EXPECT(jdir[sfFinalFields.fieldName][jss::Account] ==
env.master.human());
BEAST_EXPECT(jdir[sfFinalFields.fieldName][sfTarget.fieldName] ==
alice.human());
BEAST_EXPECT(jdir[sfFinalFields.fieldName][jss::Flags] == 0);
BEAST_EXPECT(jdir[sfFinalFields.fieldName][sfOwnerNode.fieldName] ==
"0000000000000000");
BEAST_EXPECT(jdir[sfFinalFields.fieldName][jss::Sequence] == 2);
}
void testTicketWithExpiration ()
{
testcase ("Create Ticket with Future Expiration");
using namespace test::jtx;
Env env {*this, features (featureTickets)};
// create and verify
uint32_t expire =
(env.timeKeeper ().closeTime () + 60s)
.time_since_epoch ().count ();
env (ticket::create (env.master, expire));
auto cr = checkTicketMeta (env);
auto const& jacct = cr[0];
auto const& jticket = cr[1];
BEAST_EXPECT(
jacct[sfPreviousFields.fieldName][sfOwnerCount.fieldName] == 0);
BEAST_EXPECT(
jacct[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 1);
BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Sequence] ==
jacct[sfPreviousFields.fieldName][jss::Sequence]);
BEAST_EXPECT(
jticket[sfNewFields.fieldName][sfExpiration.fieldName] == expire);
}
void testTicketZeroExpiration ()
{
testcase ("Create Ticket with Zero Expiration");
using namespace test::jtx;
Env env {*this, features (featureTickets)};
// create and verify
env (ticket::create (env.master, 0u), ter (temBAD_EXPIRATION));
}
void testTicketWithPastExpiration ()
{
testcase ("Create Ticket with Past Expiration");
using namespace test::jtx;
Env env {*this, features (featureTickets)};
env.timeKeeper ().adjustCloseTime (days {2});
env.close ();
// create and verify
uint32_t expire = 60;
env (ticket::create (env.master, expire));
// in the case of past expiration, we only get
// one meta node entry returned
auto const& jvm = env.meta ()->getJson (0);
BEAST_EXPECT(jvm.isMember(sfAffectedNodes.fieldName));
BEAST_EXPECT(jvm[sfAffectedNodes.fieldName].isArray());
BEAST_EXPECT(jvm[sfAffectedNodes.fieldName].size() == 1);
BEAST_EXPECT(jvm[sfAffectedNodes.fieldName][0u].
isMember(sfModifiedNode.fieldName));
auto const& jacct =
jvm[sfAffectedNodes.fieldName][0u][sfModifiedNode.fieldName];
BEAST_EXPECT(
jacct[sfLedgerEntryType.fieldName] == "AccountRoot");
BEAST_EXPECT(jacct[sfFinalFields.fieldName][jss::Account] ==
env.master.human());
}
void testTicketAllowExpiration ()
{
testcase ("Create Ticket and Allow to Expire");
using namespace test::jtx;
Env env {*this, features (featureTickets)};
// create and verify
uint32_t expire =
(env.timeKeeper ().closeTime () + std::chrono::hours {3})
.time_since_epoch().count();
env (ticket::create (env.master, expire));
auto cr = checkTicketMeta (env);
auto const& jacct = cr[0];
auto const& jticket = cr[1];
BEAST_EXPECT(
jacct[sfPreviousFields.fieldName][sfOwnerCount.fieldName] == 0);
BEAST_EXPECT(
jacct[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 1);
BEAST_EXPECT(
jticket[sfNewFields.fieldName][sfExpiration.fieldName] == expire);
BEAST_EXPECT(jticket[sfLedgerIndex.fieldName] ==
"7F58A0AE17775BA3404D55D406DD1C2E91EADD7AF3F03A26877BCE764CCB75E3");
Account alice {"alice"};
env.fund (XRP (10000), alice);
env.close ();
// now try to cancel with alice account, which should not work
auto jv = ticket::cancel(alice, jticket[sfLedgerIndex.fieldName].asString());
env (jv, ter (tecNO_PERMISSION));
// advance the ledger time to as to trigger expiration
env.timeKeeper ().adjustCloseTime (days {3});
env.close ();
// now try again - the cancel succeeds because ticket has expired
env (jv);
auto crd = checkTicketMeta (env, true, true);
auto const& jticketd = crd[1];
BEAST_EXPECT(
jticketd[sfFinalFields.fieldName][sfExpiration.fieldName] == expire);
}
public:
void run ()
{
testTicketNotEnabled ();
testTicketCancelNonexistent ();
testTicketCreatePreflightFail ();
testTicketCreateNonexistent ();
testTicketToSelf ();
testTicketCancelByCreator ();
testTicketInsufficientReserve ();
testTicketCancelByTarget ();
testTicketWithExpiration ();
testTicketZeroExpiration ();
testTicketWithPastExpiration ();
testTicketAllowExpiration ();
}
};
BEAST_DEFINE_TESTSUITE (Ticket, tx, ripple);
} // ripple

View File

@@ -41,3 +41,4 @@
#include <test/app/TxQ_test.cpp>
#include <test/app/ValidatorList_test.cpp>
#include <test/app/SetTrust_test.cpp>
#include <test/app/Ticket_test.cpp>