Files
rippled/src/test/app/Credentials_test.cpp
2025-03-13 11:30:30 +00:00

1110 lines
40 KiB
C++

//------------------------------------------------------------------------------
/*
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.
*/
//==============================================================================
#include <test/jtx.h>
#include <xrpld/app/misc/CredentialHelpers.h>
#include <xrpld/ledger/ApplyViewImpl.h>
#include <xrpl/basics/strHex.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 ripple {
namespace test {
static inline bool
checkVL(
std::shared_ptr<SLE const> const& sle,
SField const& field,
std::string const& expected)
{
return strHex(expected) == strHex(sle->getFieldVL(field));
}
struct Credentials_test : public beast::unit_test::suite
{
void
testSuccessful(FeatureBitset features)
{
using namespace test::jtx;
const char credType[] = "abcde";
const char 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;
const char 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()
->info()
.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;
const char 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()
->info()
.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)));
}
}
{
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().accountReserve(0));
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;
const char 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)));
}
{
const char credType2[] = "efghi";
testcase("CredentialsAccept fail, expired credentials.");
auto jv = credentials::create(subject, issuer, credType2);
uint32_t const t = env.current()
->info()
.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;
const char 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));
}
{
const char 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()
->info()
.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 =
ripple::credentials::deleteSLE(*view, {}, env.journal);
BEAST_EXPECT(ter == tecNO_ENTRY);
}
}
}
void
testFeatureFailed(FeatureBitset features)
{
using namespace test::jtx;
const char 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;
const char 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"));
const char 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{supported_amendments()};
testSuccessful(all);
testCredentialsDelete(all);
testCreateFailed(all);
testAcceptFailed(all);
testDeleteFailed(all);
testFeatureFailed(all - featureCredentials);
testFlags(all - fixInvalidTxFlags);
testFlags(all);
testRPC();
}
};
BEAST_DEFINE_TESTSUITE(Credentials, app, ripple);
} // namespace test
} // namespace ripple