diff --git a/CMakeLists.txt b/CMakeLists.txt index ade3c1a3..629db210 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,7 @@ target_sources(clio PRIVATE ## NextGen RPC handler src/rpc/ngHandlers/AccountChannels.cpp src/rpc/ngHandlers/AccountCurrencies.cpp + src/rpc/ngHandlers/AccountLines.cpp src/rpc/ngHandlers/Tx.cpp src/rpc/ngHandlers/GatewayBalances.cpp src/rpc/ngHandlers/LedgerEntry.cpp @@ -123,6 +124,7 @@ if(BUILD_TESTS) unittests/rpc/RPCHelpersTest.cpp unittests/rpc/handlers/TestHandlerTests.cpp unittests/rpc/handlers/AccountCurrenciesTest.cpp + unittests/rpc/handlers/AccountLinesTest.cpp unittests/rpc/handlers/DefaultProcessorTests.cpp unittests/rpc/handlers/PingTest.cpp unittests/rpc/handlers/AccountChannelsTest.cpp diff --git a/src/rpc/handlers/AccountLines.cpp b/src/rpc/handlers/AccountLines.cpp index b9a7c580..a44a4889 100644 --- a/src/rpc/handlers/AccountLines.cpp +++ b/src/rpc/handlers/AccountLines.cpp @@ -70,7 +70,6 @@ addLine( flags & (!viewLowest ? ripple::lsfLowAuth : ripple::lsfHighAuth); bool lineNoRipple = flags & (viewLowest ? ripple::lsfLowNoRipple : ripple::lsfHighNoRipple); - bool lineDefaultRipple = flags & ripple::lsfDefaultRipple; bool lineNoRipplePeer = flags & (!viewLowest ? ripple::lsfLowNoRipple : ripple::lsfHighNoRipple); bool lineFreeze = @@ -94,14 +93,12 @@ addLine( jPeer[JS(authorized)] = true; if (lineAuthPeer) jPeer[JS(peer_authorized)] = true; - if (lineNoRipple || !lineDefaultRipple) - jPeer[JS(no_ripple)] = lineNoRipple; - if (lineNoRipple || !lineDefaultRipple) - jPeer[JS(no_ripple_peer)] = lineNoRipplePeer; if (lineFreeze) jPeer[JS(freeze)] = true; if (lineFreezePeer) jPeer[JS(freeze_peer)] = true; + jPeer[JS(no_ripple)] = lineNoRipple; + jPeer[JS(no_ripple_peer)] = lineNoRipplePeer; jsonLines.push_back(jPeer); } diff --git a/src/rpc/ngHandlers/AccountLines.cpp b/src/rpc/ngHandlers/AccountLines.cpp new file mode 100644 index 00000000..fa624bbf --- /dev/null +++ b/src/rpc/ngHandlers/AccountLines.cpp @@ -0,0 +1,269 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace RPCng { + +void +AccountLinesHandler::addLine( + std::vector& lines, + ripple::SLE const& lineSle, + ripple::AccountID const& account, + std::optional const& peerAccount) const +{ + auto const flags = lineSle.getFieldU32(ripple::sfFlags); + auto const lowLimit = lineSle.getFieldAmount(ripple::sfLowLimit); + auto const highLimit = lineSle.getFieldAmount(ripple::sfHighLimit); + auto const lowID = lowLimit.getIssuer(); + auto const highID = highLimit.getIssuer(); + auto const lowQualityIn = lineSle.getFieldU32(ripple::sfLowQualityIn); + auto const lowQualityOut = lineSle.getFieldU32(ripple::sfLowQualityOut); + auto const highQualityIn = lineSle.getFieldU32(ripple::sfHighQualityIn); + auto const highQualityOut = lineSle.getFieldU32(ripple::sfHighQualityOut); + auto balance = lineSle.getFieldAmount(ripple::sfBalance); + + auto const viewLowest = (lowID == account); + auto const lineLimit = viewLowest ? lowLimit : highLimit; + auto const lineLimitPeer = not viewLowest ? lowLimit : highLimit; + auto const lineAccountIDPeer = not viewLowest ? lowID : highID; + auto const lineQualityIn = viewLowest ? lowQualityIn : highQualityIn; + auto const lineQualityOut = viewLowest ? lowQualityOut : highQualityOut; + + if (peerAccount && peerAccount != lineAccountIDPeer) + return; + + if (not viewLowest) + balance.negate(); + + bool const lineAuth = + flags & (viewLowest ? ripple::lsfLowAuth : ripple::lsfHighAuth); + bool const lineAuthPeer = + flags & (not viewLowest ? ripple::lsfLowAuth : ripple::lsfHighAuth); + bool const lineNoRipple = + flags & (viewLowest ? ripple::lsfLowNoRipple : ripple::lsfHighNoRipple); + bool const lineNoRipplePeer = flags & + (not viewLowest ? ripple::lsfLowNoRipple : ripple::lsfHighNoRipple); + bool const lineFreeze = + flags & (viewLowest ? ripple::lsfLowFreeze : ripple::lsfHighFreeze); + bool const lineFreezePeer = + flags & (not viewLowest ? ripple::lsfLowFreeze : ripple::lsfHighFreeze); + + ripple::STAmount const& saBalance = balance; + ripple::STAmount const& saLimit = lineLimit; + ripple::STAmount const& saLimitPeer = lineLimitPeer; + + LineResponse line; + line.account = ripple::to_string(lineAccountIDPeer); + line.balance = saBalance.getText(); + line.currency = ripple::to_string(saBalance.issue().currency); + line.limit = saLimit.getText(); + line.limitPeer = saLimitPeer.getText(); + line.qualityIn = lineQualityIn; + line.qualityOut = lineQualityOut; + if (lineAuth) + line.authorized = true; + if (lineAuthPeer) + line.peerAuthorized = true; + if (lineFreeze) + line.freeze = true; + if (lineFreezePeer) + line.freezePeer = true; + line.noRipple = lineNoRipple; + line.noRipplePeer = lineNoRipplePeer; + + lines.push_back(line); +} + +AccountLinesHandler::Result +AccountLinesHandler::process( + AccountLinesHandler::Input input, + boost::asio::yield_context& yield) const +{ + auto const range = sharedPtrBackend_->fetchLedgerRange(); + auto const lgrInfoOrStatus = RPC::getLedgerInfoFromHashOrSeq( + *sharedPtrBackend_, + yield, + input.ledgerHash, + input.ledgerIndex, + range->maxSequence); + + if (auto status = std::get_if(&lgrInfoOrStatus)) + return Error{*status}; + + auto const lgrInfo = std::get(lgrInfoOrStatus); + auto const accountID = RPC::accountFromStringStrict(input.account); + auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject( + ripple::keylet::account(*accountID).key, lgrInfo.seq, yield); + + if (not accountLedgerObject) + return Error{RPC::Status{ + RPC::RippledError::rpcACT_NOT_FOUND, "accountNotFound"}}; + + auto const peerAccountID = input.peer + ? RPC::accountFromStringStrict(*(input.peer)) + : std::optional{}; + + Output response; + response.lines.reserve(input.limit); + + auto const addToResponse = [&](ripple::SLE&& sle) { + if (sle.getType() == ripple::ltRIPPLE_STATE) + { + auto ignore = false; + if (input.ignoreDefault) + { + if (sle.getFieldAmount(ripple::sfLowLimit).getIssuer() == + accountID) + { + ignore = + !(sle.getFieldU32(ripple::sfFlags) & + ripple::lsfLowReserve); + } + else + { + ignore = + !(sle.getFieldU32(ripple::sfFlags) & + ripple::lsfHighReserve); + } + } + + if (not ignore) + addLine(response.lines, sle, *accountID, peerAccountID); + } + }; + + auto const next = RPC::ngTraverseOwnedNodes( + *sharedPtrBackend_, + *accountID, + lgrInfo.seq, + input.limit, + input.marker, + yield, + addToResponse); + + response.account = input.account; + response.limit = + input.limit; // not documented, + // https://github.com/XRPLF/xrpl-dev-portal/issues/1838 + response.ledgerHash = ripple::strHex(lgrInfo.hash); + response.ledgerIndex = lgrInfo.seq; + + auto const nextMarker = std::get(next); + if (nextMarker.isNonZero()) + response.marker = nextMarker.toString(); + + return response; +} + +AccountLinesHandler::Input +tag_invoke( + boost::json::value_to_tag, + boost::json::value const& jv) +{ + auto const& jsonObject = jv.as_object(); + AccountLinesHandler::Input input; + + input.account = jv.at(JS(account)).as_string().c_str(); + if (jsonObject.contains(JS(limit))) + { + input.limit = jv.at(JS(limit)).as_int64(); + } + if (jsonObject.contains(JS(marker))) + { + input.marker = jv.at(JS(marker)).as_string().c_str(); + } + if (jsonObject.contains(JS(ledger_hash))) + { + input.ledgerHash = jv.at(JS(ledger_hash)).as_string().c_str(); + } + if (jsonObject.contains(JS(peer))) + { + input.peer = jv.at(JS(peer)).as_string().c_str(); + } + if (jsonObject.contains(JS(ignore_default))) + { + input.ignoreDefault = jv.at(JS(ignore_default)).as_bool(); + } + if (jsonObject.contains(JS(ledger_index))) + { + if (!jsonObject.at(JS(ledger_index)).is_string()) + { + input.ledgerIndex = jv.at(JS(ledger_index)).as_int64(); + } + else if (jsonObject.at(JS(ledger_index)).as_string() != "validated") + { + input.ledgerIndex = + std::stoi(jv.at(JS(ledger_index)).as_string().c_str()); + } + } + + return input; +} + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + AccountLinesHandler::Output const& output) +{ + auto obj = boost::json::object{ + {JS(ledger_hash), output.ledgerHash}, + {JS(ledger_index), output.ledgerIndex}, + {JS(validated), output.validated}, + {JS(limit), output.limit}, + {JS(lines), output.lines}, + }; + if (output.marker) + obj[JS(marker)] = output.marker.value(); + jv = std::move(obj); +} + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + [[maybe_unused]] AccountLinesHandler::LineResponse const& line) +{ + auto obj = boost::json::object{ + {JS(account), line.account}, + {JS(balance), line.balance}, + {JS(currency), line.currency}, + {JS(limit), line.limit}, + {JS(limit_peer), line.limitPeer}, + {JS(quality_in), line.qualityIn}, + {JS(quality_out), line.qualityOut}, + }; + + obj[JS(no_ripple)] = line.noRipple; + obj[JS(no_ripple_peer)] = line.noRipplePeer; + + if (line.authorized) + obj[JS(authorized)] = *(line.authorized); + if (line.peerAuthorized) + obj[JS(peer_authorized)] = *(line.peerAuthorized); + if (line.freeze) + obj[JS(freeze)] = *(line.freeze); + if (line.freezePeer) + obj[JS(freeze_peer)] = *(line.freezePeer); + + jv = std::move(obj); +} +} // namespace RPCng diff --git a/src/rpc/ngHandlers/AccountLines.h b/src/rpc/ngHandlers/AccountLines.h new file mode 100644 index 00000000..68ce8c4d --- /dev/null +++ b/src/rpc/ngHandlers/AccountLines.h @@ -0,0 +1,135 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#pragma once + +#include +#include +#include +#include + +#include + +#include + +namespace RPCng { + +class AccountLinesHandler +{ + // dependencies + std::shared_ptr const sharedPtrBackend_; + +public: + struct LineResponse + { + std::string account; + std::string balance; + std::string currency; + std::string limit; + std::string limitPeer; + uint32_t qualityIn; + uint32_t qualityOut; + bool noRipple; + bool noRipplePeer; + std::optional authorized; + std::optional peerAuthorized; + std::optional freeze; + std::optional freezePeer; + }; + + struct Output + { + std::string account; + std::vector lines; + std::string ledgerHash; + uint32_t ledgerIndex; + bool validated = true; // should be sent via framework + std::optional marker; + uint32_t limit; + }; + + struct Input + { + std::string account; + std::optional ledgerHash; + std::optional ledgerIndex; + std::optional peer; + bool ignoreDefault = + false; // TODO: document + // https://github.com/XRPLF/xrpl-dev-portal/issues/1839 + uint32_t limit = 50; + std::optional marker; + }; + + using Result = RPCng::HandlerReturnType; + + AccountLinesHandler( + std::shared_ptr const& sharedPtrBackend) + : sharedPtrBackend_(sharedPtrBackend) + { + } + + RpcSpecConstRef + spec() const + { + // clang-format off + static auto const rpcSpec = RpcSpec{ + {JS(account), validation::Required{}, validation::AccountValidator}, + {JS(peer), validation::Type{}, validation::AccountValidator}, + {JS(ignore_default), validation::Type{}}, + {JS(ledger_hash), validation::Uint256HexStringValidator}, + {JS(limit), validation::Type{}, validation::Between{10, 400}}, + {JS(ledger_index), validation::LedgerIndexValidator}, + {JS(marker), validation::MarkerValidator}, + }; + // clang-format on + + return rpcSpec; + } + + Result + process(Input input, boost::asio::yield_context& yield) const; + +private: + void + addLine( + std::vector& lines, + ripple::SLE const& lineSle, + ripple::AccountID const& account, + std::optional const& peerAccount) const; +}; + +AccountLinesHandler::Input +tag_invoke( + boost::json::value_to_tag, + boost::json::value const& jv); + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + AccountLinesHandler::Output const& output); + +void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + AccountLinesHandler::LineResponse const& line); + +} // namespace RPCng diff --git a/unittests/rpc/handlers/AccountChannelsTest.cpp b/unittests/rpc/handlers/AccountChannelsTest.cpp index 7b3b4689..0bc611cc 100644 --- a/unittests/rpc/handlers/AccountChannelsTest.cpp +++ b/unittests/rpc/handlers/AccountChannelsTest.cpp @@ -383,7 +383,7 @@ TEST_F(RPCAccountHandlerTest, NonExistAccount) // normal case when only provide account TEST_F(RPCAccountHandlerTest, DefaultParameterTest) { - constexpr static auto correntOutput = R"({ + constexpr static auto correctOutput = R"({ "account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index":30, @@ -457,7 +457,7 @@ TEST_F(RPCAccountHandlerTest, DefaultParameterTest) auto handler = AnyHandler{AccountChannelsHandler{this->mockBackendPtr}}; auto const output = handler.process(input, yield); ASSERT_TRUE(output); - EXPECT_EQ(json::parse(correntOutput), *output); + EXPECT_EQ(json::parse(correctOutput), *output); }); ctx.run(); } @@ -640,7 +640,7 @@ TEST_F(RPCAccountHandlerTest, EmptyChannel) // Return expiration cancel_offer source_tag destination_tag when available TEST_F(RPCAccountHandlerTest, OptionalResponseField) { - constexpr static auto correntOutput = R"({ + constexpr static auto correctOutput = R"({ "account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "ledger_index":30, @@ -725,7 +725,7 @@ TEST_F(RPCAccountHandlerTest, OptionalResponseField) auto handler = AnyHandler{AccountChannelsHandler{this->mockBackendPtr}}; auto const output = handler.process(input, yield); ASSERT_TRUE(output); - EXPECT_EQ(json::parse(correntOutput), *output); + EXPECT_EQ(json::parse(correctOutput), *output); }); ctx.run(); } diff --git a/unittests/rpc/handlers/AccountLinesTest.cpp b/unittests/rpc/handlers/AccountLinesTest.cpp new file mode 100644 index 00000000..cd7a58c1 --- /dev/null +++ b/unittests/rpc/handlers/AccountLinesTest.cpp @@ -0,0 +1,905 @@ +//------------------------------------------------------------------------------ +/* + 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 RPCng; +namespace json = boost::json; +using namespace testing; + +constexpr static auto LEDGERHASH = + "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto ACCOUNT3 = "rB9BMzh27F3Q6a5FtGPDayQoCCEdiRdqcK"; +constexpr static auto INDEX1 = + "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"; +constexpr static auto INDEX2 = + "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC322"; +constexpr static auto TXNID = + "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD"; + +class RPCAccountLinesHandlerTest : public HandlerBaseTest +{ +}; + +// TODO: a lot of the tests are copy-pasted from AccountChannelsTest +// because the logic is mostly the same but currently implemented in +// a separate handler class. We should eventually use some sort of +// base class or common component to these `account_*` rpcs. + +TEST_F(RPCAccountLinesHandlerTest, NonHexLedgerHash) +{ + runSpawn([this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": 10, + "ledger_hash": "xxx" + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "ledger_hashMalformed"); + }); +} + +TEST_F(RPCAccountLinesHandlerTest, NonStringLedgerHash) +{ + runSpawn([this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": 10, + "ledger_hash": 123 + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "ledger_hashNotString"); + }); +} + +TEST_F(RPCAccountLinesHandlerTest, InvalidLedgerIndexString) +{ + runSpawn([this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": 10, + "ledger_index": "notvalidated" + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerIndexMalformed"); + }); +} + +TEST_F(RPCAccountLinesHandlerTest, MarkerNotString) +{ + runSpawn([this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "marker": 9 + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "markerNotString"); + }); +} + +// error case : invalid marker +// marker format is composed of a comma separated index and start hint. The +// former will be read as hex, and the latter using boost lexical cast. +TEST_F(RPCAccountLinesHandlerTest, InvalidMarker) +{ + runSpawn([this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "marker": "123invalid" + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "Malformed cursor"); + }); + runSpawn([this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "marker": 401 + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + }); +} + +// the limit is between 10 400 +TEST_F(RPCAccountLinesHandlerTest, IncorrectLimit) +{ + runSpawn([this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": 9 + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + }); + runSpawn([this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": 401 + }})", + ACCOUNT)); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + }); +} + +// error case: account invalid format, length is incorrect +TEST_F(RPCAccountLinesHandlerTest, AccountInvalidFormat) +{ + runSpawn([this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const input = json::parse( + R"({ + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jp" + })"); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "accountMalformed"); + }); +} + +// error case: account invalid format +TEST_F(RPCAccountLinesHandlerTest, AccountNotString) +{ + runSpawn([this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const input = json::parse( + R"({ + "account": 12 + })"); + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "accountNotString"); + }); +} + +// error case ledger non exist via hash +TEST_F(RPCAccountLinesHandlerTest, NonExistLedgerViaLedgerHash) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + // mock fetchLedgerByHash return empty + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) + .WillByDefault(Return(std::optional{})); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_hash": "{}" + }})", + ACCOUNT, + LEDGERHASH)); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); + }); +} + +// error case ledger non exist via index +TEST_F(RPCAccountLinesHandlerTest, NonExistLedgerViaLedgerIndex) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + // mock fetchLedgerBySequence return empty + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(std::optional{})); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_index": "4" + }})", + ACCOUNT)); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); + }); +} + +// error case ledger > max seq via hash +// idk why this case will happen in reality +TEST_F(RPCAccountLinesHandlerTest, NonExistLedgerViaLedgerHash2) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + // mock fetchLedgerByHash return ledger but seq is 31 > 30 + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 31); + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_hash": "{}" + }})", + ACCOUNT, + LEDGERHASH)); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); + }); +} + +// error case ledger > max seq via index +TEST_F(RPCAccountLinesHandlerTest, NonExistLedgerViaLedgerIndex2) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + // no need to check from db, call fetchLedgerBySequence 0 time + // differ from previous logic + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(0); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_index": "31" + }})", + ACCOUNT)); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); + }); +} + +// error case account not exist +TEST_F(RPCAccountLinesHandlerTest, NonExistAccount) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerByHash).Times(1); + // fetch account object return emtpy + ON_CALL(*rawBackendPtr, doFetchLedgerObject) + .WillByDefault(Return(std::optional{})); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_hash": "{}" + }})", + ACCOUNT, + LEDGERHASH)); + runSpawn([&, this](auto& yield) { + auto const handler = AnyHandler{AccountLinesHandler{mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_FALSE(output); + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "actNotFound"); + EXPECT_EQ(err.at("error_message").as_string(), "accountNotFound"); + }); +} + +// normal case when only provide account +TEST_F(RPCAccountLinesHandlerTest, DefaultParameterTest) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // fetch account object return something + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto owneDirKk = ripple::keylet::ownerDir(account).key; + auto fake = Blob{'f', 'a', 'k', 'e'}; + // return a non empty account + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + + // return owner index containing 2 indexes + ripple::STObject ownerDir = CreateOwnerDirLedgerObject( + {ripple::uint256{INDEX1}, ripple::uint256{INDEX2}}, INDEX1); + + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(owneDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + // return two trust lines + std::vector bbs; + auto const line1 = CreateRippleStateLedgerObject( + ACCOUNT, "USD", ACCOUNT2, 10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 123); + auto const line2 = CreateRippleStateLedgerObject( + ACCOUNT2, "USD", ACCOUNT, 10, ACCOUNT2, 100, ACCOUNT, 200, TXNID, 123); + bbs.push_back(line1.getSerializer().peekData()); + bbs.push_back(line2.getSerializer().peekData()); + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + runSpawn([this](auto& yield) { + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}" + }})", + ACCOUNT)); + auto const correctOutput = + R"({ + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index": 30, + "validated": true, + "limit": 50, + "lines": [ + { + "account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "balance": "10", + "currency": "USD", + "limit": "100", + "limit_peer": "200", + "quality_in": 0, + "quality_out": 0, + "no_ripple": false, + "no_ripple_peer": false + }, + { + "account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "balance": "-10", + "currency": "USD", + "limit": "200", + "limit_peer": "100", + "quality_in": 0, + "quality_out": 0, + "no_ripple": false, + "no_ripple_peer": false + } + ] + })"; + + auto handler = AnyHandler{AccountLinesHandler{this->mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ(json::parse(correctOutput), *output); + }); +} + +// normal case : limit is used +TEST_F(RPCAccountLinesHandlerTest, UseLimit) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // fetch account object return something + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto owneDirKk = ripple::keylet::ownerDir(account).key; + auto fake = Blob{'f', 'a', 'k', 'e'}; + // return a non empty account + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + + // return owner index + std::vector indexes; + std::vector bbs; + + auto repetitions = 50; + while (repetitions--) + { + indexes.push_back(ripple::uint256{INDEX1}); + auto const line = CreateRippleStateLedgerObject( + ACCOUNT, + "USD", + ACCOUNT2, + 10, + ACCOUNT, + 100, + ACCOUNT2, + 200, + TXNID, + 123); + bbs.push_back(line.getSerializer().peekData()); + } + ripple::STObject ownerDir = CreateOwnerDirLedgerObject(indexes, INDEX1); + // it should not appear in return marker,marker is the current page + ownerDir.setFieldU64(ripple::sfIndexNext, 99); + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(owneDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": 20 + }})", + ACCOUNT)); + runSpawn([&, this](auto& yield) { + auto handler = AnyHandler{AccountLinesHandler{this->mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + + EXPECT_EQ((*output).as_object().at("lines").as_array().size(), 20); + EXPECT_THAT( + (*output).as_object().at("marker").as_string().c_str(), + EndsWith(",0")); + }); +} + +// normal case : destination is used +TEST_F(RPCAccountLinesHandlerTest, UseDestination) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // fetch account object return something + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto owneDirKk = ripple::keylet::ownerDir(account).key; + auto fake = Blob{'f', 'a', 'k', 'e'}; + // return a non empty account + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + + // return owner index + std::vector indexes; + std::vector bbs; + + // 10 lines to ACCOUNT2 + auto repetitions = 10; + while (repetitions--) + { + indexes.push_back(ripple::uint256{INDEX1}); + auto const line = CreateRippleStateLedgerObject( + ACCOUNT, + "USD", + ACCOUNT2, + 10, + ACCOUNT, + 100, + ACCOUNT2, + 200, + TXNID, + 123); + bbs.push_back(line.getSerializer().peekData()); + } + + // 20 lines to ACCOUNT3 + repetitions = 20; + while (repetitions--) + { + indexes.push_back(ripple::uint256{INDEX1}); + auto const line = CreateRippleStateLedgerObject( + ACCOUNT, + "USD", + ACCOUNT3, + 10, + ACCOUNT, + 100, + ACCOUNT3, + 200, + TXNID, + 123); + bbs.push_back(line.getSerializer().peekData()); + } + + ripple::STObject ownerDir = CreateOwnerDirLedgerObject(indexes, INDEX1); + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(owneDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": 30, + "peer": "{}" + }})", + ACCOUNT, + ACCOUNT3)); + runSpawn([&, this](auto& yield) { + auto handler = AnyHandler{AccountLinesHandler{this->mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ((*output).as_object().at("lines").as_array().size(), 20); + }); +} + +// normal case : but the lines is emtpy +TEST_F(RPCAccountLinesHandlerTest, EmptyChannel) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // fetch account object return something + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto owneDirKk = ripple::keylet::ownerDir(account).key; + auto fake = Blob{'f', 'a', 'k', 'e'}; + // return a non empty account + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + + // return owner index + ripple::STObject ownerDir = CreateOwnerDirLedgerObject({}, INDEX1); + + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(owneDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}" + }})", + ACCOUNT)); + runSpawn([&, this](auto& yield) { + auto handler = AnyHandler{AccountLinesHandler{this->mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ((*output).as_object().at("lines").as_array().size(), 0); + }); +} + +TEST_F(RPCAccountLinesHandlerTest, OptionalResponseField) +{ + constexpr static auto correctOutput = R"({ + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_index": 30, + "validated": true, + "limit": 50, + "lines": [ + { + "account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "balance": "10", + "currency": "USD", + "limit": "100", + "limit_peer": "200", + "quality_in": 0, + "quality_out": 0, + "no_ripple": false, + "no_ripple_peer": true, + "peer_authorized": true, + "freeze_peer": true + }, + { + "account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "balance": "20", + "currency": "USD", + "limit": "200", + "limit_peer": "400", + "quality_in": 0, + "quality_out": 0, + "no_ripple": true, + "no_ripple_peer": false, + "authorized": true, + "freeze": true + } + ] + })"; + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // fetch account object return something + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + auto owneDirKk = ripple::keylet::ownerDir(account).key; + auto fake = Blob{'f', 'a', 'k', 'e'}; + + // return a non empty account + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(accountKk, testing::_, testing::_)) + .WillByDefault(Return(fake)); + + // 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())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + // return few trust lines + std::vector bbs; + auto line1 = CreateRippleStateLedgerObject( + ACCOUNT, "USD", ACCOUNT2, 10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 0); + line1.setFlag(ripple::lsfHighAuth); + line1.setFlag(ripple::lsfHighNoRipple); + line1.setFlag(ripple::lsfHighFreeze); + bbs.push_back(line1.getSerializer().peekData()); + + auto line2 = CreateRippleStateLedgerObject( + ACCOUNT, "USD", ACCOUNT2, 20, ACCOUNT, 200, ACCOUNT2, 400, TXNID, 0); + line2.setFlag(ripple::lsfLowAuth); + line2.setFlag(ripple::lsfLowNoRipple); + line2.setFlag(ripple::lsfLowFreeze); + bbs.push_back(line2.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}" + }})", + ACCOUNT)); + runSpawn([&, this](auto& yield) { + auto handler = AnyHandler{AccountLinesHandler{this->mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ(json::parse(correctOutput), *output); + }); +} + +// normal case : test marker output correct +TEST_F(RPCAccountLinesHandlerTest, MarkerOutput) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + 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; + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // 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; + auto line = CreateRippleStateLedgerObject( + ACCOUNT, "USD", ACCOUNT2, 10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 0); + + // owner dir contains 10 indexes + int objectsCount = 10; + std::vector indexes; + while (objectsCount != 0) + { + // return owner index + indexes.push_back(ripple::uint256{INDEX1}); + objectsCount--; + } + // return 15 objects + objectsCount = 15; + while (objectsCount != 0) + { + bbs.push_back(line.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); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": {} + }})", + ACCOUNT, + limit)); + runSpawn([&, this](auto& yield) { + auto handler = AnyHandler{AccountLinesHandler{this->mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_EQ( + (*output).as_object().at("marker").as_string().c_str(), + fmt::format("{},{}", INDEX1, nextPage)); + EXPECT_EQ((*output).as_object().at("lines").as_array().size(), 15); + }); +} + +// normal case : handler marker correctly +TEST_F(RPCAccountLinesHandlerTest, MarkerInput) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto account = GetAccountIDWithString(ACCOUNT); + auto accountKk = ripple::keylet::account(account).key; + constexpr static auto nextPage = 99; + constexpr static auto limit = 15; + auto ownerDirKk = + ripple::keylet::page(ripple::keylet::ownerDir(account), nextPage).key; + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // 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; + auto const line = CreateRippleStateLedgerObject( + ACCOUNT, "USD", ACCOUNT2, 10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 0); + int objectsCount = limit; + std::vector indexes; + while (objectsCount != 0) + { + // return owner index + indexes.push_back(ripple::uint256{INDEX1}); + bbs.push_back(line.getSerializer().peekData()); + objectsCount--; + } + + ripple::STObject ownerDir = CreateOwnerDirLedgerObject(indexes, INDEX1); + ownerDir.setFieldU64(ripple::sfIndexNext, 0); + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(ownerDirKk, testing::_, testing::_)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const input = json::parse(fmt::format( + R"({{ + "account": "{}", + "limit": {}, + "marker": "{},{}" + }})", + ACCOUNT, + limit, + INDEX1, + nextPage)); + runSpawn([&, this](auto& yield) { + auto handler = AnyHandler{AccountLinesHandler{this->mockBackendPtr}}; + auto const output = handler.process(input, yield); + ASSERT_TRUE(output); + EXPECT_TRUE((*output).as_object().if_contains("marker") == nullptr); + // the first item is the marker itself, so the result will have limit-1 + // items + EXPECT_EQ( + (*output).as_object().at("lines").as_array().size(), limit - 1); + }); +} diff --git a/unittests/util/Fixtures.h b/unittests/util/Fixtures.h index 88607787..8b0de05f 100644 --- a/unittests/util/Fixtures.h +++ b/unittests/util/Fixtures.h @@ -165,6 +165,7 @@ struct SyncAsioContextTest : virtual public NoLoggerFixture }); ctx.run(); ASSERT_TRUE(called); + ctx.reset(); } protected: