diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj index a060d9f4d8..40d06f1df9 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj +++ b/Builds/VisualStudio2015/RippleD.vcxproj @@ -4402,6 +4402,10 @@ True True + + True + True + True True diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters index c274f87049..8a1925f805 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters @@ -5205,6 +5205,9 @@ test\app + + test\app + test\app diff --git a/src/ripple/test/jtx/impl/ticket.cpp b/src/ripple/test/jtx/impl/ticket.cpp index 075dd8d9c7..d080bd6951 100644 --- a/src/ripple/test/jtx/impl/ticket.cpp +++ b/src/ripple/test/jtx/impl/ticket.cpp @@ -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 diff --git a/src/ripple/test/jtx/ticket.h b/src/ripple/test/jtx/ticket.h index 813e35b566..1aea80dd6d 100644 --- a/src/ripple/test/jtx/ticket.h +++ b/src/ripple/test/jtx/ticket.h @@ -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 diff --git a/src/test/app/Ticket_test.cpp b/src/test/app/Ticket_test.cpp new file mode 100644 index 0000000000..fcacba35a6 --- /dev/null +++ b/src/test/app/Ticket_test.cpp @@ -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 +#include +#include +#include + +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 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 + > 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 + diff --git a/src/unity/app_test_unity.cpp b/src/unity/app_test_unity.cpp index fc8f417e0e..e736107a0c 100644 --- a/src/unity/app_test_unity.cpp +++ b/src/unity/app_test_unity.cpp @@ -41,3 +41,4 @@ #include #include #include +#include