text: //------------------------------------------------------------------------------
  /*
    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 <ripple/protocol/Feature.h>
  #include <ripple/protocol/jss.h>
  #include <test/jtx.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(JsonOptions::none);
          bool is_cancel = tx[jss::TransactionType] == jss::TicketCancel;
  
          auto const& jvm = env.meta()->getJson(JsonOptions::none);
          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, Json::StaticString>>
              expected_nodes;
  
          if (is_cancel && other_target)
          {
              expected_nodes = {
                  {0, sfModifiedNode.fieldName, jss::AccountRoot},
                  {expiration ? 2 : 1,
                   sfModifiedNode.fieldName,
                   jss::AccountRoot},
                  {expiration ? 1 : 2, sfDeletedNode.fieldName, jss::Ticket},
                  {3, sfDeletedNode.fieldName, jss::DirectoryNode}};
          }
          else
          {
              expected_nodes = {
                  {0, sfModifiedNode.fieldName, jss::AccountRoot},
                  {1,
                   is_cancel ? sfDeletedNode.fieldName : sfCreatedNode.fieldName,
                   jss::Ticket},
                  {2,
                   is_cancel ? sfDeletedNode.fieldName : sfCreatedNode.fieldName,
                   jss::DirectoryNode}};
          }
  
          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, FeatureBitset{}};
  
          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, supported_amendments().set(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, supported_amendments().set(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, supported_amendments().set(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, supported_amendments().set(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, supported_amendments().set(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, supported_amendments().set(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, supported_amendments().set(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] == jss::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, supported_amendments().set(featureTickets)};
  
          // create and verify
          using namespace std::chrono_literals;
          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, supported_amendments().set(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, supported_amendments().set(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(JsonOptions::none);
          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] == jss::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, supported_amendments().set(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() override
      {
          testTicketNotEnabled();
          testTicketCancelNonexistent();
          testTicketCreatePreflightFail();
          testTicketCreateNonexistent();
          testTicketToSelf();
          testTicketCancelByCreator();
          testTicketInsufficientReserve();
          testTicketCancelByTarget();
          testTicketWithExpiration();
          testTicketZeroExpiration();
          testTicketWithPastExpiration();
          testTicketAllowExpiration();
      }
  };
  
  BEAST_DEFINE_TESTSUITE(Ticket, tx, ripple);
  
  }  // namespace ripple
  