Files
rippled/src/test/app/Credentials_test.cpp
2026-02-26 17:09:20 -05:00

1032 lines
39 KiB
C++

#include <test/jtx.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/ledger/ApplyViewImpl.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <string_view>
namespace xrpl {
namespace test {
struct Credentials_test : public beast::unit_test::suite
{
void
testSuccessful(FeatureBitset features)
{
using namespace test::jtx;
char const credType[] = "abcde";
char const uri[] = "uri";
Account const issuer{"issuer"};
Account const subject{"subject"};
Account const other{"other"};
Env env{*this, features};
{
testcase("Create for subject.");
auto const credKey = credentials::keylet(subject, issuer, credType);
env.fund(XRP(5000), subject, issuer, other);
env.close();
// Test Create credentials
env(credentials::create(subject, issuer, credType), credentials::uri(uri));
env.close();
{
auto const sleCred = env.le(credKey);
BEAST_EXPECT(static_cast<bool>(sleCred));
if (!sleCred)
return;
BEAST_EXPECT(sleCred->getAccountID(sfSubject) == subject.id());
BEAST_EXPECT(sleCred->getAccountID(sfIssuer) == issuer.id());
BEAST_EXPECT(!sleCred->getFieldU32(sfFlags));
BEAST_EXPECT(ownerCount(env, issuer) == 1);
BEAST_EXPECT(!ownerCount(env, subject));
BEAST_EXPECT(checkVL(sleCred, sfCredentialType, credType));
BEAST_EXPECT(checkVL(sleCred, sfURI, uri));
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
!jle[jss::result].isMember(jss::error) &&
jle[jss::result].isMember(jss::node) &&
jle[jss::result][jss::node].isMember("LedgerEntryType") &&
jle[jss::result][jss::node]["LedgerEntryType"] == jss::Credential &&
jle[jss::result][jss::node][jss::Issuer] == issuer.human() &&
jle[jss::result][jss::node][jss::Subject] == subject.human() &&
jle[jss::result][jss::node]["CredentialType"] ==
strHex(std::string_view(credType)));
}
env(credentials::accept(subject, issuer, credType));
env.close();
{
// check switching owner of the credentials from issuer to
// subject
auto const sleCred = env.le(credKey);
BEAST_EXPECT(static_cast<bool>(sleCred));
if (!sleCred)
return;
BEAST_EXPECT(sleCred->getAccountID(sfSubject) == subject.id());
BEAST_EXPECT(sleCred->getAccountID(sfIssuer) == issuer.id());
BEAST_EXPECT(!ownerCount(env, issuer));
BEAST_EXPECT(ownerCount(env, subject) == 1);
BEAST_EXPECT(checkVL(sleCred, sfCredentialType, credType));
BEAST_EXPECT(checkVL(sleCred, sfURI, uri));
BEAST_EXPECT(sleCred->getFieldU32(sfFlags) == lsfAccepted);
}
env(credentials::deleteCred(subject, subject, issuer, credType));
env.close();
{
BEAST_EXPECT(!env.le(credKey));
BEAST_EXPECT(!ownerCount(env, issuer));
BEAST_EXPECT(!ownerCount(env, subject));
// check no credential exists anymore
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
jle[jss::result].isMember(jss::error) &&
jle[jss::result][jss::error] == "entryNotFound");
}
}
{
testcase("Create for themself.");
auto const credKey = credentials::keylet(issuer, issuer, credType);
env(credentials::create(issuer, issuer, credType), credentials::uri(uri));
env.close();
{
auto const sleCred = env.le(credKey);
BEAST_EXPECT(static_cast<bool>(sleCred));
if (!sleCred)
return;
BEAST_EXPECT(sleCred->getAccountID(sfSubject) == issuer.id());
BEAST_EXPECT(sleCred->getAccountID(sfIssuer) == issuer.id());
BEAST_EXPECT((sleCred->getFieldU32(sfFlags) & lsfAccepted));
BEAST_EXPECT(
sleCred->getFieldU64(sfIssuerNode) == sleCred->getFieldU64(sfSubjectNode));
BEAST_EXPECT(ownerCount(env, issuer) == 1);
BEAST_EXPECT(checkVL(sleCred, sfCredentialType, credType));
BEAST_EXPECT(checkVL(sleCred, sfURI, uri));
auto const jle = credentials::ledgerEntry(env, issuer, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
!jle[jss::result].isMember(jss::error) &&
jle[jss::result].isMember(jss::node) &&
jle[jss::result][jss::node].isMember("LedgerEntryType") &&
jle[jss::result][jss::node]["LedgerEntryType"] == jss::Credential &&
jle[jss::result][jss::node][jss::Issuer] == issuer.human() &&
jle[jss::result][jss::node][jss::Subject] == issuer.human() &&
jle[jss::result][jss::node]["CredentialType"] ==
strHex(std::string_view(credType)));
}
env(credentials::deleteCred(issuer, issuer, issuer, credType));
env.close();
{
BEAST_EXPECT(!env.le(credKey));
BEAST_EXPECT(!ownerCount(env, issuer));
// check no credential exists anymore
auto const jle = credentials::ledgerEntry(env, issuer, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
jle[jss::result].isMember(jss::error) &&
jle[jss::result][jss::error] == "entryNotFound");
}
}
}
void
testCredentialsDelete(FeatureBitset features)
{
using namespace test::jtx;
char const credType[] = "abcde";
Account const issuer{"issuer"};
Account const subject{"subject"};
Account const other{"other"};
Env env{*this, features};
// fund subject and issuer
env.fund(XRP(5000), issuer, subject, other);
env.close();
{
testcase("Delete issuer before accept");
auto const credKey = credentials::keylet(subject, issuer, credType);
env(credentials::create(subject, issuer, credType));
env.close();
// delete issuer
{
int const delta = env.seq(issuer) + 255;
for (int i = 0; i < delta; ++i)
env.close();
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(issuer, other), fee(acctDelFee));
env.close();
}
// check credentials deleted too
{
BEAST_EXPECT(!env.le(credKey));
BEAST_EXPECT(!ownerCount(env, subject));
// check no credential exists anymore
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
jle[jss::result].isMember(jss::error) &&
jle[jss::result][jss::error] == "entryNotFound");
}
// resurrection
env.fund(XRP(5000), issuer);
env.close();
}
{
testcase("Delete issuer after accept");
auto const credKey = credentials::keylet(subject, issuer, credType);
env(credentials::create(subject, issuer, credType));
env.close();
env(credentials::accept(subject, issuer, credType));
env.close();
// delete issuer
{
int const delta = env.seq(issuer) + 255;
for (int i = 0; i < delta; ++i)
env.close();
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(issuer, other), fee(acctDelFee));
env.close();
}
// check credentials deleted too
{
BEAST_EXPECT(!env.le(credKey));
BEAST_EXPECT(!ownerCount(env, subject));
// check no credential exists anymore
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
jle[jss::result].isMember(jss::error) &&
jle[jss::result][jss::error] == "entryNotFound");
}
// resurrection
env.fund(XRP(5000), issuer);
env.close();
}
{
testcase("Delete subject before accept");
auto const credKey = credentials::keylet(subject, issuer, credType);
env(credentials::create(subject, issuer, credType));
env.close();
// delete subject
{
int const delta = env.seq(subject) + 255;
for (int i = 0; i < delta; ++i)
env.close();
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(subject, other), fee(acctDelFee));
env.close();
}
// check credentials deleted too
{
BEAST_EXPECT(!env.le(credKey));
BEAST_EXPECT(!ownerCount(env, issuer));
// check no credential exists anymore
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
jle[jss::result].isMember(jss::error) &&
jle[jss::result][jss::error] == "entryNotFound");
}
// resurrection
env.fund(XRP(5000), subject);
env.close();
}
{
testcase("Delete subject after accept");
auto const credKey = credentials::keylet(subject, issuer, credType);
env(credentials::create(subject, issuer, credType));
env.close();
env(credentials::accept(subject, issuer, credType));
env.close();
// delete subject
{
int const delta = env.seq(subject) + 255;
for (int i = 0; i < delta; ++i)
env.close();
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(subject, other), fee(acctDelFee));
env.close();
}
// check credentials deleted too
{
BEAST_EXPECT(!env.le(credKey));
BEAST_EXPECT(!ownerCount(env, issuer));
// check no credential exists anymore
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
jle[jss::result].isMember(jss::error) &&
jle[jss::result][jss::error] == "entryNotFound");
}
// resurrection
env.fund(XRP(5000), subject);
env.close();
}
{
testcase("Delete by other");
auto const credKey = credentials::keylet(subject, issuer, credType);
auto jv = credentials::create(subject, issuer, credType);
uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count();
jv[sfExpiration.jsonName] = t + 20;
env(jv);
// time advance
env.close();
env.close();
env.close();
// Other account delete credentials
env(credentials::deleteCred(other, subject, issuer, credType));
env.close();
// check credentials object
{
BEAST_EXPECT(!env.le(credKey));
BEAST_EXPECT(!ownerCount(env, issuer));
BEAST_EXPECT(!ownerCount(env, subject));
// check no credential exists anymore
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
jle[jss::result].isMember(jss::error) &&
jle[jss::result][jss::error] == "entryNotFound");
}
}
{
testcase("Delete by subject");
env(credentials::create(subject, issuer, credType));
env.close();
// Subject can delete
env(credentials::deleteCred(subject, subject, issuer, credType));
env.close();
{
auto const credKey = credentials::keylet(subject, issuer, credType);
BEAST_EXPECT(!env.le(credKey));
BEAST_EXPECT(!ownerCount(env, subject));
BEAST_EXPECT(!ownerCount(env, issuer));
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
jle[jss::result].isMember(jss::error) &&
jle[jss::result][jss::error] == "entryNotFound");
}
}
{
testcase("Delete by issuer");
env(credentials::create(subject, issuer, credType));
env.close();
env(credentials::deleteCred(issuer, subject, issuer, credType));
env.close();
{
auto const credKey = credentials::keylet(subject, issuer, credType);
BEAST_EXPECT(!env.le(credKey));
BEAST_EXPECT(!ownerCount(env, subject));
BEAST_EXPECT(!ownerCount(env, issuer));
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
jle[jss::result].isMember(jss::error) &&
jle[jss::result][jss::error] == "entryNotFound");
}
}
}
void
testCreateFailed(FeatureBitset features)
{
using namespace test::jtx;
char const credType[] = "abcde";
Account const issuer{"issuer"};
Account const subject{"subject"};
{
using namespace jtx;
Env env{*this, features};
env.fund(XRP(5000), subject, issuer);
env.close();
{
testcase("Credentials fail, no subject param.");
auto jv = credentials::create(subject, issuer, credType);
jv.removeMember(jss::Subject);
env(jv, ter(temMALFORMED));
}
{
auto jv = credentials::create(subject, issuer, credType);
jv[jss::Subject] = to_string(xrpAccount());
env(jv, ter(temMALFORMED));
}
{
testcase("Credentials fail, no credentialType param.");
auto jv = credentials::create(subject, issuer, credType);
jv.removeMember(sfCredentialType.jsonName);
env(jv, ter(temMALFORMED));
}
{
testcase("Credentials fail, empty credentialType param.");
auto jv = credentials::create(subject, issuer, "");
env(jv, ter(temMALFORMED));
}
{
testcase(
"Credentials fail, credentialType length > "
"maxCredentialTypeLength.");
constexpr std::string_view longCredType =
"abcdefghijklmnopqrstuvwxyz01234567890qwertyuiop[]"
"asdfghjkl;'zxcvbnm8237tr28weufwldebvfv8734t07p";
static_assert(longCredType.size() > maxCredentialTypeLength);
auto jv = credentials::create(subject, issuer, longCredType);
env(jv, ter(temMALFORMED));
}
{
testcase("Credentials fail, URI length > 256.");
constexpr std::string_view longURI =
"abcdefghijklmnopqrstuvwxyz01234567890qwertyuiop[]"
"asdfghjkl;'zxcvbnm8237tr28weufwldebvfv8734t07p "
"9hfup;wDJFBVSD8f72 "
"pfhiusdovnbs;"
"djvbldafghwpEFHdjfaidfgio84763tfysgdvhjasbd "
"vujhgWQIE7F6WEUYFGWUKEYFVQW87FGWOEFWEFUYWVEF8723GFWEFB"
"WULE"
"fv28o37gfwEFB3872TFO8GSDSDVD";
static_assert(longURI.size() > maxCredentialURILength);
env(credentials::create(subject, issuer, credType),
credentials::uri(longURI),
ter(temMALFORMED));
}
{
testcase("Credentials fail, URI empty.");
env(credentials::create(subject, issuer, credType),
credentials::uri(""),
ter(temMALFORMED));
}
{
testcase("Credentials fail, expiration in the past.");
auto jv = credentials::create(subject, issuer, credType);
// current time in ripple epoch - 1s
uint32_t const t =
env.current()->header().parentCloseTime.time_since_epoch().count() - 1;
jv[sfExpiration.jsonName] = t;
env(jv, ter(tecEXPIRED));
}
{
testcase("Credentials fail, invalid fee.");
auto jv = credentials::create(subject, issuer, credType);
jv[jss::Fee] = -1;
env(jv, ter(temBAD_FEE));
}
{
testcase("Credentials fail, duplicate.");
auto const jv = credentials::create(subject, issuer, credType);
env(jv);
env.close();
env(jv, ter(tecDUPLICATE));
env.close();
// check credential still present
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
!jle[jss::result].isMember(jss::error) &&
jle[jss::result].isMember(jss::node) &&
jle[jss::result][jss::node].isMember("LedgerEntryType") &&
jle[jss::result][jss::node]["LedgerEntryType"] == jss::Credential &&
jle[jss::result][jss::node][jss::Issuer] == issuer.human() &&
jle[jss::result][jss::node][jss::Subject] == subject.human() &&
jle[jss::result][jss::node]["CredentialType"] ==
strHex(std::string_view(credType)));
}
{
testcase("Credentials fail, directory full");
std::uint32_t const issuerSeq{env.seq(issuer) + 1};
env(ticket::create(issuer, 63));
env.close();
// Everything below can only be tested on open ledger.
auto const res1 = directory::bumpLastPage(
env,
directory::maximumPageIndex(env),
keylet::ownerDir(issuer.id()),
directory::adjustOwnerNode);
BEAST_EXPECT(res1);
auto const jv = credentials::create(issuer, subject, credType);
env(jv, ter(tecDIR_FULL));
// Free one directory entry by using a ticket
env(noop(issuer), ticket::use(issuerSeq + 40));
// Fill subject directory
env(ticket::create(subject, 63));
auto const res2 = directory::bumpLastPage(
env,
directory::maximumPageIndex(env),
keylet::ownerDir(subject.id()),
directory::adjustOwnerNode);
BEAST_EXPECT(res2);
env(jv, ter(tecDIR_FULL));
// End test
env.close();
}
}
{
using namespace jtx;
Env env{*this, features};
env.fund(XRP(5000), issuer);
env.close();
{
testcase("Credentials fail, subject doesn't exist.");
auto const jv = credentials::create(subject, issuer, credType);
env(jv, ter(tecNO_TARGET));
}
}
{
using namespace jtx;
Env env{*this, features};
auto const reserve = drops(env.current()->fees().reserve);
env.fund(reserve, subject, issuer);
env.close();
testcase("Credentials fail, not enough reserve.");
{
auto const jv = credentials::create(subject, issuer, credType);
env(jv, ter(tecINSUFFICIENT_RESERVE));
env.close();
}
}
}
void
testAcceptFailed(FeatureBitset features)
{
using namespace jtx;
char const credType[] = "abcde";
Account const issuer{"issuer"};
Account const subject{"subject"};
Account const other{"other"};
{
Env env{*this, features};
env.fund(XRP(5000), subject, issuer);
{
testcase("CredentialsAccept fail, Credential doesn't exist.");
env(credentials::accept(subject, issuer, credType), ter(tecNO_ENTRY));
env.close();
}
{
testcase("CredentialsAccept fail, invalid Issuer account.");
auto jv = credentials::accept(subject, issuer, credType);
jv[jss::Issuer] = to_string(xrpAccount());
env(jv, ter(temINVALID_ACCOUNT_ID));
env.close();
}
{
testcase("CredentialsAccept fail, invalid credentialType param.");
auto jv = credentials::accept(subject, issuer, "");
env(jv, ter(temMALFORMED));
}
}
{
Env env{*this, features};
env.fund(drops(env.current()->fees().accountReserve(1)), issuer);
env.fund(drops(env.current()->fees().accountReserve(0)), subject);
env.close();
{
testcase("CredentialsAccept fail, not enough reserve.");
env(credentials::create(subject, issuer, credType));
env.close();
env(credentials::accept(subject, issuer, credType), ter(tecINSUFFICIENT_RESERVE));
env.close();
// check credential still present
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
!jle[jss::result].isMember(jss::error) &&
jle[jss::result].isMember(jss::node) &&
jle[jss::result][jss::node].isMember("LedgerEntryType") &&
jle[jss::result][jss::node]["LedgerEntryType"] == jss::Credential &&
jle[jss::result][jss::node][jss::Issuer] == issuer.human() &&
jle[jss::result][jss::node][jss::Subject] == subject.human() &&
jle[jss::result][jss::node]["CredentialType"] ==
strHex(std::string_view(credType)));
}
}
{
using namespace jtx;
Env env{*this, features};
env.fund(XRP(5000), subject, issuer);
env.close();
{
env(credentials::create(subject, issuer, credType));
env.close();
testcase("CredentialsAccept fail, invalid fee.");
auto jv = credentials::accept(subject, issuer, credType);
jv[jss::Fee] = -1;
env(jv, ter(temBAD_FEE));
testcase("CredentialsAccept fail, lsfAccepted already set.");
env(credentials::accept(subject, issuer, credType));
env.close();
env(credentials::accept(subject, issuer, credType), ter(tecDUPLICATE));
env.close();
// check credential still present
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
!jle[jss::result].isMember(jss::error) &&
jle[jss::result].isMember(jss::node) &&
jle[jss::result][jss::node].isMember("LedgerEntryType") &&
jle[jss::result][jss::node]["LedgerEntryType"] == jss::Credential &&
jle[jss::result][jss::node][jss::Issuer] == issuer.human() &&
jle[jss::result][jss::node][jss::Subject] == subject.human() &&
jle[jss::result][jss::node]["CredentialType"] ==
strHex(std::string_view(credType)));
}
{
char const credType2[] = "efghi";
testcase("CredentialsAccept fail, expired credentials.");
auto jv = credentials::create(subject, issuer, credType2);
uint32_t const t =
env.current()->header().parentCloseTime.time_since_epoch().count();
jv[sfExpiration.jsonName] = t;
env(jv);
env.close();
// credentials are expired now
env(credentials::accept(subject, issuer, credType2), ter(tecEXPIRED));
env.close();
// check that expired credentials were deleted
auto const jDelCred = credentials::ledgerEntry(env, subject, issuer, credType2);
BEAST_EXPECT(
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
jDelCred[jss::result].isMember(jss::error) &&
jDelCred[jss::result][jss::error] == "entryNotFound");
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, subject) == 1);
}
}
{
using namespace jtx;
Env env{*this, features};
env.fund(XRP(5000), issuer, subject, other);
env.close();
{
testcase("CredentialsAccept fail, issuer doesn't exist.");
auto jv = credentials::create(subject, issuer, credType);
env(jv);
env.close();
// delete issuer
int const delta = env.seq(issuer) + 255;
for (int i = 0; i < delta; ++i)
env.close();
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(issuer, other), fee(acctDelFee));
// can't accept - no issuer account
jv = credentials::accept(subject, issuer, credType);
env(jv, ter(tecNO_ISSUER));
env.close();
// check that expired credentials were deleted
auto const jDelCred = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
jDelCred[jss::result].isMember(jss::error) &&
jDelCred[jss::result][jss::error] == "entryNotFound");
}
}
}
void
testDeleteFailed(FeatureBitset features)
{
using namespace test::jtx;
char const credType[] = "abcde";
Account const issuer{"issuer"};
Account const subject{"subject"};
Account const other{"other"};
{
using namespace jtx;
Env env{*this, features};
env.fund(XRP(5000), subject, issuer, other);
env.close();
{
testcase("CredentialsDelete fail, no Credentials.");
env(credentials::deleteCred(subject, subject, issuer, credType), ter(tecNO_ENTRY));
env.close();
}
{
testcase("CredentialsDelete fail, invalid Subject account.");
auto jv = credentials::deleteCred(subject, subject, issuer, credType);
jv[jss::Subject] = to_string(xrpAccount());
env(jv, ter(temINVALID_ACCOUNT_ID));
env.close();
}
{
testcase("CredentialsDelete fail, invalid Issuer account.");
auto jv = credentials::deleteCred(subject, subject, issuer, credType);
jv[jss::Issuer] = to_string(xrpAccount());
env(jv, ter(temINVALID_ACCOUNT_ID));
env.close();
}
{
testcase("CredentialsDelete fail, invalid credentialType param.");
auto jv = credentials::deleteCred(subject, subject, issuer, "");
env(jv, ter(temMALFORMED));
}
{
char const credType2[] = "fghij";
env(credentials::create(subject, issuer, credType2));
env.close();
// Other account can't delete credentials without expiration
env(credentials::deleteCred(other, subject, issuer, credType2),
ter(tecNO_PERMISSION));
env.close();
// check credential still present
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType2);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
!jle[jss::result].isMember(jss::error) &&
jle[jss::result].isMember(jss::node) &&
jle[jss::result][jss::node].isMember("LedgerEntryType") &&
jle[jss::result][jss::node]["LedgerEntryType"] == jss::Credential &&
jle[jss::result][jss::node][jss::Issuer] == issuer.human() &&
jle[jss::result][jss::node][jss::Subject] == subject.human() &&
jle[jss::result][jss::node]["CredentialType"] ==
strHex(std::string_view(credType2)));
}
{
testcase("CredentialsDelete fail, time not expired yet.");
auto jv = credentials::create(subject, issuer, credType);
// current time in ripple epoch + 1000s
uint32_t const t =
env.current()->header().parentCloseTime.time_since_epoch().count() + 1000;
jv[sfExpiration.jsonName] = t;
env(jv);
env.close();
// Other account can't delete credentials that not expired
env(credentials::deleteCred(other, subject, issuer, credType),
ter(tecNO_PERMISSION));
env.close();
// check credential still present
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) &&
!jle[jss::result].isMember(jss::error) &&
jle[jss::result].isMember(jss::node) &&
jle[jss::result][jss::node].isMember("LedgerEntryType") &&
jle[jss::result][jss::node]["LedgerEntryType"] == jss::Credential &&
jle[jss::result][jss::node][jss::Issuer] == issuer.human() &&
jle[jss::result][jss::node][jss::Subject] == subject.human() &&
jle[jss::result][jss::node]["CredentialType"] ==
strHex(std::string_view(credType)));
}
{
testcase("CredentialsDelete fail, no Issuer and Subject.");
auto jv = credentials::deleteCred(subject, subject, issuer, credType);
jv.removeMember(jss::Subject);
jv.removeMember(jss::Issuer);
env(jv, ter(temMALFORMED));
env.close();
}
{
testcase("CredentialsDelete fail, invalid fee.");
auto jv = credentials::deleteCred(subject, subject, issuer, credType);
jv[jss::Fee] = -1;
env(jv, ter(temBAD_FEE));
env.close();
}
{
testcase("deleteSLE fail, bad SLE.");
auto view =
std::make_shared<ApplyViewImpl>(env.current().get(), ApplyFlags::tapNONE);
auto ter = xrpl::credentials::deleteSLE(*view, {}, env.journal);
BEAST_EXPECT(ter == tecNO_ENTRY);
}
}
}
void
testFeatureFailed(FeatureBitset features)
{
using namespace test::jtx;
char const credType[] = "abcde";
Account const issuer{"issuer"};
Account const subject{"subject"};
{
using namespace jtx;
Env env{*this, features};
env.fund(XRP(5000), subject, issuer);
env.close();
{
testcase("Credentials fail, Feature is not enabled.");
env(credentials::create(subject, issuer, credType), ter(temDISABLED));
env(credentials::accept(subject, issuer, credType), ter(temDISABLED));
env(credentials::deleteCred(subject, subject, issuer, credType), ter(temDISABLED));
}
}
}
void
testRPC()
{
using namespace test::jtx;
char const credType[] = "abcde";
Account const issuer{"issuer"};
Account const subject{"subject"};
{
using namespace jtx;
Env env{*this};
env.fund(XRP(5000), subject, issuer);
env.close();
env(credentials::create(subject, issuer, credType));
env.close();
env(credentials::accept(subject, issuer, credType));
env.close();
testcase("account_tx");
std::string txHash0, txHash1;
{
Json::Value params;
params[jss::account] = subject.human();
auto const jv = env.rpc("json", "account_tx", to_string(params))[jss::result];
BEAST_EXPECT(jv[jss::transactions].size() == 4);
auto const& tx0(jv[jss::transactions][0u][jss::tx]);
BEAST_EXPECT(tx0[jss::TransactionType] == jss::CredentialAccept);
auto const& tx1(jv[jss::transactions][1u][jss::tx]);
BEAST_EXPECT(tx1[jss::TransactionType] == jss::CredentialCreate);
txHash0 = tx0[jss::hash].asString();
txHash1 = tx1[jss::hash].asString();
}
{
Json::Value params;
params[jss::account] = issuer.human();
auto const jv = env.rpc("json", "account_tx", to_string(params))[jss::result];
BEAST_EXPECT(jv[jss::transactions].size() == 4);
auto const& tx0(jv[jss::transactions][0u][jss::tx]);
BEAST_EXPECT(tx0[jss::TransactionType] == jss::CredentialAccept);
auto const& tx1(jv[jss::transactions][1u][jss::tx]);
BEAST_EXPECT(tx1[jss::TransactionType] == jss::CredentialCreate);
BEAST_EXPECT(txHash0 == tx0[jss::hash].asString());
BEAST_EXPECT(txHash1 == tx1[jss::hash].asString());
}
testcase("account_objects");
std::string objectIdx;
{
Json::Value params;
params[jss::account] = subject.human();
auto jv = env.rpc("json", "account_objects", to_string(params))[jss::result];
BEAST_EXPECT(jv[jss::account_objects].size() == 1);
auto const& object(jv[jss::account_objects][0u]);
BEAST_EXPECT(object["LedgerEntryType"].asString() == jss::Credential);
objectIdx = object[jss::index].asString();
}
{
Json::Value params;
params[jss::account] = issuer.human();
auto jv = env.rpc("json", "account_objects", to_string(params))[jss::result];
BEAST_EXPECT(jv[jss::account_objects].size() == 1);
auto const& object(jv[jss::account_objects][0u]);
BEAST_EXPECT(object["LedgerEntryType"].asString() == jss::Credential);
BEAST_EXPECT(objectIdx == object[jss::index].asString());
}
}
}
void
testFlags(FeatureBitset features)
{
using namespace test::jtx;
bool const enabled = features[fixInvalidTxFlags];
testcase(std::string("Test flag, fix ") + (enabled ? "enabled" : "disabled"));
char const credType[] = "abcde";
Account const issuer{"issuer"};
Account const subject{"subject"};
{
using namespace jtx;
Env env{*this, features};
env.fund(XRP(5000), subject, issuer);
env.close();
{
ter const expected(enabled ? TER(temINVALID_FLAG) : TER(tesSUCCESS));
env(credentials::create(subject, issuer, credType),
txflags(tfTransferable),
expected);
env(credentials::accept(subject, issuer, credType),
txflags(tfSellNFToken),
expected);
env(credentials::deleteCred(subject, subject, issuer, credType),
txflags(tfPassive),
expected);
}
}
}
void
run() override
{
using namespace test::jtx;
FeatureBitset const all{testable_amendments()};
testSuccessful(all);
testCredentialsDelete(all);
testCreateFailed(all);
testCreateFailed(all - fixDirectoryLimit);
testAcceptFailed(all);
testDeleteFailed(all);
testFeatureFailed(all - featureCredentials);
testFlags(all - fixInvalidTxFlags);
testFlags(all);
testRPC();
}
};
BEAST_DEFINE_TESTSUITE(Credentials, app, xrpl);
} // namespace test
} // namespace xrpl