diff --git a/CMakeLists.txt b/CMakeLists.txt index a6578a1d..d975010f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,6 +112,7 @@ if(BUILD_TESTS) unittests/util/TestObject.cpp unittests/rpc/ErrorTests.cpp unittests/rpc/BaseTests.cpp + unittests/rpc/RPCHelpersTest.cpp unittests/rpc/handlers/TestHandlerTests.cpp unittests/rpc/handlers/DefaultProcessorTests.cpp unittests/rpc/handlers/PingTest.cpp) diff --git a/src/rpc/RPCHelpers.cpp b/src/rpc/RPCHelpers.cpp index bc56443f..6608636d 100644 --- a/src/rpc/RPCHelpers.cpp +++ b/src/rpc/RPCHelpers.cpp @@ -680,6 +680,8 @@ traverseOwnedNodes( auto const rootIndex = owner; auto currentIndex = rootIndex; + // track the current page we are accessing, will return it as the next hint + auto currentPage = startHint; std::vector keys; // Only reserve 2048 nodes when fetching all owned ledger objects. If there @@ -742,18 +744,18 @@ traverseOwnedNodes( } } - auto const uNodeNext = sle.getFieldU64(ripple::sfIndexNext); - if (limit == 0) { - cursor = AccountCursor({keys.back(), uNodeNext}); + cursor = AccountCursor({keys.back(), currentPage}); break; } - + // the next page + auto const uNodeNext = sle.getFieldU64(ripple::sfIndexNext); if (uNodeNext == 0) break; currentIndex = ripple::keylet::page(rootIndex, uNodeNext); + currentPage = uNodeNext; } } else @@ -777,18 +779,18 @@ traverseOwnedNodes( break; } - auto const uNodeNext = sle.getFieldU64(ripple::sfIndexNext); - if (limit == 0) { - cursor = AccountCursor({keys.back(), uNodeNext}); + cursor = AccountCursor({keys.back(), currentPage}); break; } + auto const uNodeNext = sle.getFieldU64(ripple::sfIndexNext); if (uNodeNext == 0) break; currentIndex = ripple::keylet::page(rootIndex, uNodeNext); + currentPage = uNodeNext; } } auto end = std::chrono::system_clock::now(); diff --git a/unittests/rpc/RPCHelpersTest.cpp b/unittests/rpc/RPCHelpersTest.cpp new file mode 100644 index 00000000..f71e06e6 --- /dev/null +++ b/unittests/rpc/RPCHelpersTest.cpp @@ -0,0 +1,417 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and 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 + +#include + +using namespace RPC; +using namespace testing; + +constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto INDEX1 = + "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"; +constexpr static auto INDEX2 = + "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322"; +constexpr static auto TXNID = + "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"; + +class RPCHelpersTest : public MockBackendTest, public SyncAsioContextTest +{ + void + SetUp() override + { + MockBackendTest::SetUp(); + SyncAsioContextTest::SetUp(); + } + void + TearDown() override + { + MockBackendTest::TearDown(); + SyncAsioContextTest::TearDown(); + } +}; + +TEST_F(RPCHelpersTest, TraverseOwnedNodesNotAccount) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + // fetch account object return emtpy + ON_CALL(*rawBackendPtr, doFetchLedgerObject) + .WillByDefault(Return(std::optional{})); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto account = GetAccountIDWithString(ACCOUNT); + auto ret = traverseOwnedNodes( + *mockBackendPtr, account, 9, 10, "", yield, [](auto) { + + }); + auto status = std::get_if(&ret); + EXPECT_TRUE(status != nullptr); + EXPECT_EQ(*status, RippledError::rpcACT_NOT_FOUND); + }); + ctx.run(); +} + +TEST_F(RPCHelpersTest, TraverseOwnedNodesMarkerInvalidIndexNotHex) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + // fetch account object return something + auto fake = Blob{'f', 'a', 'k', 'e'}; + ON_CALL(*rawBackendPtr, doFetchLedgerObject).WillByDefault(Return(fake)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto account = GetAccountIDWithString(ACCOUNT); + auto ret = traverseOwnedNodes( + *mockBackendPtr, account, 9, 10, "nothex,10", yield, [](auto) { + + }); + auto status = std::get_if(&ret); + EXPECT_TRUE(status != nullptr); + EXPECT_EQ(*status, ripple::rpcINVALID_PARAMS); + EXPECT_EQ(status->message, "Malformed cursor"); + }); + ctx.run(); +} + +TEST_F(RPCHelpersTest, TraverseOwnedNodesMarkerInvalidPageNotInt) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + // fetch account object return something + auto fake = Blob{'f', 'a', 'k', 'e'}; + ON_CALL(*rawBackendPtr, doFetchLedgerObject).WillByDefault(Return(fake)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto account = GetAccountIDWithString(ACCOUNT); + auto ret = traverseOwnedNodes( + *mockBackendPtr, account, 9, 10, "nothex,abc", yield, [](auto) { + + }); + auto status = std::get_if(&ret); + EXPECT_TRUE(status != nullptr); + EXPECT_EQ(*status, ripple::rpcINVALID_PARAMS); + EXPECT_EQ(status->message, "Malformed cursor"); + }); + ctx.run(); +} + +// limit = 10, return 2 objects +TEST_F(RPCHelpersTest, TraverseOwnedNodesNoInputMarker) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto owneDirKk = ripple::keylet::ownerDir(account).key; + // fetch account object return something + auto fake = Blob{'f', 'a', 'k', 'e'}; + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + // return owner index + ripple::STObject ownerDir = CreateOwnerDirLedgerObject( + {ripple::uint256{INDEX1}, ripple::uint256{INDEX2}}, INDEX1); + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(owneDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + + // return two payment channel objects + std::vector bbs; + ripple::STObject channel1 = CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT2, 100, 10, 32, TXNID, 28); + bbs.push_back(channel1.getSerializer().peekData()); + bbs.push_back(channel1.getSerializer().peekData()); + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + boost::asio::spawn(ctx, [this, &account](boost::asio::yield_context yield) { + auto ret = traverseOwnedNodes( + *mockBackendPtr, account, 9, 10, {}, yield, [](auto) { + + }); + auto cursor = std::get_if(&ret); + EXPECT_TRUE(cursor != nullptr); + EXPECT_EQ( + cursor->toString(), + "0000000000000000000000000000000000000000000000000000000000000000," + "0"); + }); + ctx.run(); +} + +// limit = 10, return 10 objects and marker +TEST_F(RPCHelpersTest, TraverseOwnedNodesNoInputMarkerReturnSamePageMarker) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto owneDirKk = ripple::keylet::ownerDir(account).key; + // fetch account object return something + auto fake = Blob{'f', 'a', 'k', 'e'}; + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + std::vector bbs; + + int objectsCount = 11; + ripple::STObject channel1 = CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT2, 100, 10, 32, TXNID, 28); + std::vector indexes; + while (objectsCount != 0) + { + // return owner index + indexes.push_back(ripple::uint256{INDEX1}); + bbs.push_back(channel1.getSerializer().peekData()); + objectsCount--; + } + + ripple::STObject ownerDir = CreateOwnerDirLedgerObject(indexes, INDEX1); + ownerDir.setFieldU64(ripple::sfIndexNext, 99); + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(owneDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + boost::asio::spawn(ctx, [this, &account](boost::asio::yield_context yield) { + auto count = 0; + auto ret = traverseOwnedNodes( + *mockBackendPtr, account, 9, 10, {}, yield, [&](auto) { count++; }); + auto cursor = std::get_if(&ret); + EXPECT_TRUE(cursor != nullptr); + EXPECT_EQ(count, 10); + EXPECT_EQ(cursor->toString(), fmt::format("{},0", INDEX1)); + }); + ctx.run(); +} + +// 10 objects per page, limit is 15, return the second page as marker +TEST_F(RPCHelpersTest, TraverseOwnedNodesNoInputMarkerReturnOtherPageMarker) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto ownerDirKk = ripple::keylet::ownerDir(account).key; + constexpr static auto nextPage = 99; + constexpr static auto limit = 15; + auto ownerDir2Kk = + ripple::keylet::page(ripple::keylet::ownerDir(account), nextPage).key; + + // fetch account object return something + auto fake = Blob{'f', 'a', 'k', 'e'}; + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + + std::vector bbs; + + int objectsCount = 10; + ripple::STObject channel1 = CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT2, 100, 10, 32, TXNID, 28); + std::vector indexes; + while (objectsCount != 0) + { + // return owner index + indexes.push_back(ripple::uint256{INDEX1}); + objectsCount--; + } + objectsCount = 15; + while (objectsCount != 0) + { + bbs.push_back(channel1.getSerializer().peekData()); + objectsCount--; + } + + ripple::STObject ownerDir = CreateOwnerDirLedgerObject(indexes, INDEX1); + ownerDir.setFieldU64(ripple::sfIndexNext, nextPage); + // first page 's next page is 99 + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(ownerDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + ripple::STObject ownerDir2 = CreateOwnerDirLedgerObject(indexes, INDEX1); + // second page's next page is 0 + ownerDir2.setFieldU64(ripple::sfIndexNext, 0); + ON_CALL( + *rawBackendPtr, + doFetchLedgerObject(ownerDir2Kk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir2.getSerializer().peekData())); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto count = 0; + auto ret = traverseOwnedNodes( + *mockBackendPtr, account, 9, limit, {}, yield, [&](auto) { + count++; + }); + auto cursor = std::get_if(&ret); + EXPECT_TRUE(cursor != nullptr); + EXPECT_EQ(count, limit); + EXPECT_EQ(cursor->toString(), fmt::format("{},{}", INDEX1, nextPage)); + }); + ctx.run(); +} + +// Send a valid marker +TEST_F(RPCHelpersTest, TraverseOwnedNodesWithMarkerReturnSamePageMarker) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto ownerDir2Kk = + ripple::keylet::page(ripple::keylet::ownerDir(account), 99).key; + constexpr static auto limit = 8; + constexpr static auto pageNum = 99; + // fetch account object return something + auto fake = Blob{'f', 'a', 'k', 'e'}; + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + + std::vector bbs; + + int objectsCount = 10; + ripple::STObject channel1 = CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT2, 100, 10, 32, TXNID, 28); + std::vector indexes; + while (objectsCount != 0) + { + // return owner index + indexes.push_back(ripple::uint256{INDEX1}); + objectsCount--; + } + objectsCount = 10; + while (objectsCount != 0) + { + bbs.push_back(channel1.getSerializer().peekData()); + objectsCount--; + } + + ripple::STObject ownerDir = CreateOwnerDirLedgerObject(indexes, INDEX1); + ownerDir.setFieldU64(ripple::sfIndexNext, 0); + // return ownerdir when search by marker + ON_CALL( + *rawBackendPtr, + doFetchLedgerObject(ownerDir2Kk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto count = 0; + auto ret = traverseOwnedNodes( + *mockBackendPtr, + account, + 9, + limit, + fmt::format("{},{}", INDEX1, pageNum), + yield, + [&](auto) { count++; }); + auto cursor = std::get_if(&ret); + EXPECT_TRUE(cursor != nullptr); + EXPECT_EQ(count, limit); + EXPECT_EQ(cursor->toString(), fmt::format("{},{}", INDEX1, pageNum)); + }); + ctx.run(); +} + +// Send a valid marker, but marker contain an unexisting index +// return empty +TEST_F(RPCHelpersTest, TraverseOwnedNodesWithUnexistingIndexMarker) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto ownerDir2Kk = + ripple::keylet::page(ripple::keylet::ownerDir(account), 99).key; + constexpr static auto limit = 8; + constexpr static auto pageNum = 99; + // fetch account object return something + auto fake = Blob{'f', 'a', 'k', 'e'}; + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + int objectsCount = 10; + ripple::STObject channel1 = CreatePaymentChannelLedgerObject( + ACCOUNT, ACCOUNT2, 100, 10, 32, TXNID, 28); + std::vector indexes; + while (objectsCount != 0) + { + // return owner index + indexes.push_back(ripple::uint256{INDEX1}); + objectsCount--; + } + ripple::STObject ownerDir = CreateOwnerDirLedgerObject(indexes, INDEX1); + ownerDir.setFieldU64(ripple::sfIndexNext, 0); + // return ownerdir when search by marker + ON_CALL( + *rawBackendPtr, + doFetchLedgerObject(ownerDir2Kk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + + boost::asio::spawn(ctx, [&, this](boost::asio::yield_context yield) { + auto count = 0; + auto ret = traverseOwnedNodes( + *mockBackendPtr, + account, + 9, + limit, + fmt::format("{},{}", INDEX2, pageNum), + yield, + [&](auto) { count++; }); + auto cursor = std::get_if(&ret); + EXPECT_TRUE(cursor != nullptr); + EXPECT_EQ(count, 0); + EXPECT_EQ( + cursor->toString(), + "00000000000000000000000000000000000000000000000000000000000000" + "00,0"); + }); + ctx.run(); +} diff --git a/unittests/util/Fixtures.h b/unittests/util/Fixtures.h index d42a9b30..5a9cea95 100644 --- a/unittests/util/Fixtures.h +++ b/unittests/util/Fixtures.h @@ -32,7 +32,7 @@ /** * @brief Fixture with LogService support. */ -class LoggerFixture : public ::testing::Test +class LoggerFixture : virtual public ::testing::Test { /** * @brief A simple string buffer that can be used to mock std::cout for @@ -101,7 +101,7 @@ protected: * * This is meant to be used as a base for other fixtures. */ -class NoLoggerFixture : public LoggerFixture +class NoLoggerFixture : virtual public LoggerFixture { protected: void @@ -117,7 +117,7 @@ protected: * * This is meant to be used as a base for other fixtures. */ -struct AsyncAsioContextTest : public NoLoggerFixture +struct AsyncAsioContextTest : virtual public NoLoggerFixture { AsyncAsioContextTest() { @@ -146,7 +146,7 @@ private: * Use `run_for(duration)` etc. directly on `ctx`. * This is meant to be used as a base for other fixtures. */ -struct SyncAsioContextTest : public NoLoggerFixture +struct SyncAsioContextTest : virtual public NoLoggerFixture { SyncAsioContextTest() { @@ -159,7 +159,7 @@ protected: /** * @brief Fixture with an mock backend */ -struct MockBackendTest : public NoLoggerFixture +struct MockBackendTest : virtual public NoLoggerFixture { void SetUp() override diff --git a/unittests/util/TestObject.cpp b/unittests/util/TestObject.cpp index 56c8f666..ed029cf5 100644 --- a/unittests/util/TestObject.cpp +++ b/unittests/util/TestObject.cpp @@ -243,3 +243,45 @@ CreateMetaDataForCancelOffer( metaObj.setFieldU32(ripple::sfTransactionIndex, transactionIndex); return metaObj; } + +ripple::STObject +CreateOwnerDirLedgerObject( + std::vector indexes, + std::string_view rootIndex) +{ + ripple::STObject ownerDir(ripple::sfLedgerEntry); + ownerDir.setFieldU16(ripple::sfLedgerEntryType, ripple::ltDIR_NODE); + ownerDir.setFieldV256(ripple::sfIndexes, ripple::STVector256{indexes}); + ownerDir.setFieldH256(ripple::sfRootIndex, ripple::uint256{rootIndex}); + ownerDir.setFieldU32(ripple::sfFlags, 0); + return ownerDir; +} + +ripple::STObject +CreatePaymentChannelLedgerObject( + std::string_view accountId, + std::string_view destId, + int amount, + int balance, + uint32_t settleDelay, + std::string_view previousTxnId, + uint32_t previousTxnSeq) +{ + ripple::STObject channel(ripple::sfLedgerEntry); + channel.setFieldU16(ripple::sfLedgerEntryType, ripple::ltPAYCHAN); + channel.setAccountID(ripple::sfAccount, GetAccountIDWithString(accountId)); + channel.setAccountID(ripple::sfDestination, GetAccountIDWithString(destId)); + channel.setFieldAmount(ripple::sfAmount, ripple::STAmount(amount, false)); + channel.setFieldAmount(ripple::sfBalance, ripple::STAmount(balance, false)); + channel.setFieldU32(ripple::sfSettleDelay, settleDelay); + channel.setFieldU64(ripple::sfOwnerNode, 0); + channel.setFieldH256( + ripple::sfPreviousTxnID, ripple::uint256{previousTxnId}); + channel.setFieldU32(ripple::sfPreviousTxnLgrSeq, previousTxnSeq); + channel.setFieldU32(ripple::sfFlags, 0); + uint8_t key[33] = {0}; + key[0] = 2; // KeyType::secp256k1 + ripple::Slice slice(key, 33); + channel.setFieldVL(ripple::sfPublicKey, slice); + return channel; +} diff --git a/unittests/util/TestObject.h b/unittests/util/TestObject.h index e8093c55..0bf1a09b 100644 --- a/unittests/util/TestObject.h +++ b/unittests/util/TestObject.h @@ -135,3 +135,24 @@ CreateMetaDataForCancelOffer( uint32_t transactionIndex, int finalTakerGets, int finalTakerPays); + +/* + * Create a owner dir ledger object + */ +[[nodiscard]] ripple::STObject +CreateOwnerDirLedgerObject( + std::vector indexes, + std::string_view rootIndex); + +/* + * Create a payment channel ledger object + */ +[[nodiscard]] ripple::STObject +CreatePaymentChannelLedgerObject( + std::string_view accountId, + std::string_view destId, + int amount, + int balance, + uint32_t settleDelay, + std::string_view previousTxnId, + uint32_t previousTxnSeq);