#include "data/Types.hpp" #include "rpc/Errors.hpp" #include "rpc/common/AnyHandler.hpp" #include "rpc/common/Types.hpp" #include "rpc/handlers/NFTHistory.hpp" #include "util/HandlerBaseTestFixture.hpp" #include "util/NameGenerator.hpp" #include "util/TestObject.hpp" #include #include #include #include #include #include #include #include #include #include using namespace rpc; using namespace data; using namespace testing; namespace { constexpr auto kMinSeq = 10; constexpr auto kMaxSeq = 30; constexpr auto kAccount = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; constexpr auto kAccount2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; constexpr auto kLedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; constexpr auto kNftId = "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004"; constexpr auto kApiVersion = 2; } // namespace struct RPCNFTHistoryHandlerTest : HandlerBaseTest { RPCNFTHistoryHandlerTest() { backend_->setRange(kMinSeq, kMaxSeq); } }; struct NFTHistoryParamTestCaseBundle { std::string testName; std::string testJson; std::string expectedError; std::string expectedErrorMessage; }; // parameterized test cases for parameters check struct NFTHistoryParameterTest : public RPCNFTHistoryHandlerTest, public WithParamInterface {}; static auto generateTestValuesForParametersTest() { return std::vector{ NFTHistoryParamTestCaseBundle{ .testName = "MissingNFTID", .testJson = R"JSON({})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Required field 'nft_id' missing" }, NFTHistoryParamTestCaseBundle{ .testName = "BinaryNotBool", .testJson = R"JSON({"nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "binary": 1})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, NFTHistoryParamTestCaseBundle{ .testName = "ForwardNotBool", .testJson = R"JSON({"nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "forward": 1})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, NFTHistoryParamTestCaseBundle{ .testName = "ledger_index_minNotInt", .testJson = R"JSON({"nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_index_min": "x"})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, NFTHistoryParamTestCaseBundle{ .testName = "ledger_index_maxNotInt", .testJson = R"JSON({"nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_index_max": "x"})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, NFTHistoryParamTestCaseBundle{ .testName = "ledger_indexInvalid", .testJson = R"JSON({"nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_index": "x"})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "ledgerIndexMalformed" }, NFTHistoryParamTestCaseBundle{ .testName = "ledger_hashInvalid", .testJson = R"JSON({"nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_hash": "x"})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "ledger_hashMalformed" }, NFTHistoryParamTestCaseBundle{ .testName = "ledger_hashNotString", .testJson = R"JSON({"nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_hash": 123})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "ledger_hashNotString" }, NFTHistoryParamTestCaseBundle{ .testName = "limitNotInt", .testJson = R"JSON({"nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "limit": "123"})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, NFTHistoryParamTestCaseBundle{ .testName = "limitNegative", .testJson = R"JSON({"nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "limit": -1})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, NFTHistoryParamTestCaseBundle{ .testName = "limitZero", .testJson = R"JSON({"nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "limit": 0})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, NFTHistoryParamTestCaseBundle{ .testName = "MarkerNotObject", .testJson = R"JSON({"nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "marker": 101})JSON", .expectedError = "invalidParams", .expectedErrorMessage = "invalidMarker" }, NFTHistoryParamTestCaseBundle{ .testName = "MarkerMissingSeq", .testJson = R"JSON({ "nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "marker": {"ledger": 123} })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Required field 'seq' missing" }, NFTHistoryParamTestCaseBundle{ .testName = "MarkerMissingLedger", .testJson = R"JSON({ "nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "marker": {"seq": 123} })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Required field 'ledger' missing" }, NFTHistoryParamTestCaseBundle{ .testName = "MarkerLedgerNotInt", .testJson = R"JSON({ "nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "marker": { "seq": "string", "ledger": 1 } })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, NFTHistoryParamTestCaseBundle{ .testName = "MarkerSeqNotInt", .testJson = R"JSON({ "nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "marker": { "ledger": "string", "seq": 1 } })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "Invalid parameters." }, NFTHistoryParamTestCaseBundle{ .testName = "LedgerIndexMinLessThanMinSeq", .testJson = R"JSON({ "nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_index_min": 9 })JSON", .expectedError = "lgrIdxMalformed", .expectedErrorMessage = "ledgerSeqMinOutOfRange" }, NFTHistoryParamTestCaseBundle{ .testName = "LedgerIndexMaxLargeThanMaxSeq", .testJson = R"JSON({ "nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_index_max": 31 })JSON", .expectedError = "lgrIdxMalformed", .expectedErrorMessage = "ledgerSeqMaxOutOfRange" }, NFTHistoryParamTestCaseBundle{ .testName = "LedgerIndexMaxLessThanLedgerIndexMin", .testJson = R"JSON({ "nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_index_max": 11, "ledger_index_min": 20 })JSON", .expectedError = "lgrIdxsInvalid", .expectedErrorMessage = "Ledger indexes invalid." }, NFTHistoryParamTestCaseBundle{ .testName = "LedgerIndexMaxMinAndLedgerIndex", .testJson = R"JSON({ "nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_index_max": 20, "ledger_index_min": 11, "ledger_index": 10 })JSON", .expectedError = "invalidParams", .expectedErrorMessage = "containsLedgerSpecifierAndRange" }, }; } INSTANTIATE_TEST_CASE_P( RPCNFTHistoryGroup1, NFTHistoryParameterTest, ValuesIn(generateTestValuesForParametersTest()), tests::util::kNameGenerator ); TEST_P(NFTHistoryParameterTest, InvalidParams) { auto const testBundle = GetParam(); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; auto const req = boost::json::parse(testBundle.testJson); auto const output = handler.process(req, Context{yield}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), testBundle.expectedError); EXPECT_EQ(err.at("error_message").as_string(), testBundle.expectedErrorMessage); }); } static std::vector genTransactions(uint32_t seq1, uint32_t seq2) { auto transactions = std::vector{}; auto trans1 = TransactionAndMetadata(); xrpl::STObject const obj = createPaymentTransactionObject(kAccount, kAccount2, 1, 1, 32); trans1.transaction = obj.getSerializer().peekData(); trans1.ledgerSequence = seq1; xrpl::STObject const metaObj = createPaymentTransactionMetaObject(kAccount, kAccount2, 22, 23); trans1.metadata = metaObj.getSerializer().peekData(); trans1.date = 1; transactions.push_back(trans1); auto trans2 = TransactionAndMetadata(); xrpl::STObject const obj2 = createPaymentTransactionObject(kAccount, kAccount2, 1, 1, 32); trans2.transaction = obj.getSerializer().peekData(); trans2.ledgerSequence = seq2; xrpl::STObject const metaObj2 = createPaymentTransactionMetaObject(kAccount, kAccount2, 22, 23); trans2.metadata = metaObj2.getSerializer().peekData(); trans2.date = 2; transactions.push_back(trans2); return transactions; } TEST_F(RPCNFTHistoryHandlerTest, IndexSpecificForwardTrue) { auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; ON_CALL(*backend_, fetchNFTTransactions).WillByDefault(Return(transCursor)); EXPECT_CALL( *backend_, fetchNFTTransactions( testing::_, testing::_, true, testing::Optional(testing::Eq(TransactionsCursor{kMinSeq + 1, 0})), testing::_ ) ) .Times(1); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_index_min": {}, "ledger_index_max": {}, "forward": true }})JSON", kNftId, kMinSeq + 1, kMaxSeq - 1 ) ); auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->at("nft_id").as_string(), kNftId); EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq + 1); EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq - 1); EXPECT_EQ( output.result->at("marker").as_object(), boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") ); EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); EXPECT_FALSE(output.result->as_object().contains("limit")); }); } TEST_F(RPCNFTHistoryHandlerTest, IndexSpecificForwardFalseV1) { constexpr auto kOutput = R"JSON({ "nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_index_min": 11, "ledger_index_max": 29, "transactions": [ { "meta": { "AffectedNodes": [ { "ModifiedNode": { "FinalFields": { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Balance": "22" }, "LedgerEntryType": "AccountRoot" } }, { "ModifiedNode": { "FinalFields": { "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "Balance": "23" }, "LedgerEntryType": "AccountRoot" } } ], "TransactionIndex": 0, "TransactionResult": "tesSUCCESS", "delivered_amount": "unavailable" }, "tx": { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Amount": "1", "Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "Fee": "1", "Sequence": 32, "SigningPubKey": "74657374", "TransactionType": "Payment", "hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", "DeliverMax": "1", "ledger_index": 11, "date": 1 }, "validated": true }, { "meta": { "AffectedNodes": [ { "ModifiedNode": { "FinalFields": { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Balance": "22" }, "LedgerEntryType": "AccountRoot" } }, { "ModifiedNode": { "FinalFields": { "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "Balance": "23" }, "LedgerEntryType": "AccountRoot" } } ], "TransactionIndex": 0, "TransactionResult": "tesSUCCESS", "delivered_amount": "unavailable" }, "tx": { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Amount": "1", "Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "Fee": "1", "Sequence": 32, "SigningPubKey": "74657374", "TransactionType": "Payment", "hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", "DeliverMax": "1", "ledger_index": 29, "date": 2 }, "validated": true } ], "validated": true, "marker": { "ledger": 12, "seq": 34 } })JSON"; auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; ON_CALL(*backend_, fetchNFTTransactions).WillByDefault(Return(transCursor)); EXPECT_CALL( *backend_, fetchNFTTransactions( testing::_, testing::_, false, testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq - 1, INT32_MAX})), testing::_ ) ) .Times(1); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_index_min": {}, "ledger_index_max": {}, "forward": false }})JSON", kNftId, kMinSeq + 1, kMaxSeq - 1 ) ); auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result.value(), boost::json::parse(kOutput)); }); } TEST_F(RPCNFTHistoryHandlerTest, IndexSpecificForwardFalseV2) { constexpr auto kOutput = R"JSON({ "nft_id": "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004", "ledger_index_min": 11, "ledger_index_max": 29, "transactions": [ { "meta": { "AffectedNodes": [ { "ModifiedNode": { "FinalFields": { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Balance": "22" }, "LedgerEntryType": "AccountRoot" } }, { "ModifiedNode": { "FinalFields": { "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "Balance": "23" }, "LedgerEntryType": "AccountRoot" } } ], "TransactionIndex": 0, "TransactionResult": "tesSUCCESS", "delivered_amount": "unavailable" }, "tx_json": { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "Fee": "1", "Sequence": 32, "SigningPubKey": "74657374", "TransactionType": "Payment", "DeliverMax": "1", "ledger_index": 11, "date": 1 }, "hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", "ledger_index": 11, "close_time_iso": "2000-01-01T00:00:00Z", "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "validated": true }, { "meta": { "AffectedNodes": [ { "ModifiedNode": { "FinalFields": { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Balance": "22" }, "LedgerEntryType": "AccountRoot" } }, { "ModifiedNode": { "FinalFields": { "Account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "Balance": "23" }, "LedgerEntryType": "AccountRoot" } } ], "TransactionIndex": 0, "TransactionResult": "tesSUCCESS", "delivered_amount": "unavailable" }, "tx_json": { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Destination": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "Fee": "1", "Sequence": 32, "SigningPubKey": "74657374", "TransactionType": "Payment", "DeliverMax": "1", "ledger_index": 29, "date": 2 }, "hash": "51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", "ledger_index": 29, "close_time_iso": "2000-01-01T00:00:00Z", "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", "validated": true } ], "validated": true, "marker": { "ledger": 12, "seq": 34 } })JSON"; auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; EXPECT_CALL( *backend_, fetchNFTTransactions( testing::_, testing::_, false, testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq - 1, INT32_MAX})), testing::_ ) ) .WillOnce(Return(transCursor)); auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq); ON_CALL(*backend_, fetchLedgerBySequence).WillByDefault(Return(ledgerHeader)); EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(2); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_index_min": {}, "ledger_index_max": {}, "forward": false }})JSON", kNftId, kMinSeq + 1, kMaxSeq - 1 ) ); auto const output = handler.process(kInput, Context{.yield = yield, .apiVersion = kApiVersion}); ASSERT_TRUE(output); EXPECT_EQ(output.result.value(), boost::json::parse(kOutput)); }); } TEST_F(RPCNFTHistoryHandlerTest, IndexNotSpecificForwardTrue) { auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; ON_CALL(*backend_, fetchNFTTransactions).WillByDefault(Return(transCursor)); EXPECT_CALL( *backend_, fetchNFTTransactions( testing::_, testing::_, true, testing::Optional(testing::Eq(TransactionsCursor{kMinSeq, 0})), testing::_ ) ) .Times(1); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_index_min": {}, "ledger_index_max": {}, "forward": true }})JSON", kNftId, -1, -1 ) ); auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->at("nft_id").as_string(), kNftId); EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq); EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq); EXPECT_EQ( output.result->at("marker").as_object(), boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") ); EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); EXPECT_FALSE(output.result->as_object().contains("limit")); }); } TEST_F(RPCNFTHistoryHandlerTest, IndexNotSpecificForwardFalse) { auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; ON_CALL(*backend_, fetchNFTTransactions).WillByDefault(Return(transCursor)); EXPECT_CALL( *backend_, fetchNFTTransactions( testing::_, testing::_, false, testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq, INT32_MAX})), testing::_ ) ) .Times(1); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_index_min": {}, "ledger_index_max": {}, "forward": false }})JSON", kNftId, -1, -1 ) ); auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->at("nft_id").as_string(), kNftId); EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq); EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq); EXPECT_EQ( output.result->at("marker").as_object(), boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") ); EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); EXPECT_FALSE(output.result->as_object().contains("limit")); }); } TEST_F(RPCNFTHistoryHandlerTest, BinaryTrueV1) { auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; ON_CALL(*backend_, fetchNFTTransactions).WillByDefault(Return(transCursor)); EXPECT_CALL( *backend_, fetchNFTTransactions( testing::_, testing::_, false, testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq, INT32_MAX})), testing::_ ) ) .Times(1); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_index_min": {}, "ledger_index_max": {}, "binary": true }})JSON", kNftId, -1, -1 ) ); auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->at("nft_id").as_string(), kNftId); EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq); EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq); EXPECT_EQ( output.result->at("marker").as_object(), boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") ); EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); EXPECT_EQ( output.result->at("transactions").as_array()[0].as_object().at("meta").as_string(), "201C00000000F8E5110061E762400000000000001681144B4E9C06F24296074F7B" "C48F92A97916C6DC5EA9E1E1E5110061E76240000000000000178114D31252CF90" "2EF8DD8451243869B38667CBD89DF3E1E1F1031000" ); EXPECT_EQ( output.result->at("transactions").as_array()[0].as_object().at("tx_blob").as_string(), "120000240000002061400000000000000168400000000000000173047465737481" "144B4E9C06F24296074F7BC48F92A97916C6DC5EA98314D31252CF902EF8DD8451" "243869B38667CBD89DF3" ); EXPECT_EQ( output.result->at("transactions").as_array()[0].as_object().at("date").as_uint64(), 1 ); EXPECT_FALSE(output.result->as_object().contains("limit")); }); } TEST_F(RPCNFTHistoryHandlerTest, BinaryTrueV2) { auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; EXPECT_CALL( *backend_, fetchNFTTransactions( testing::_, testing::_, false, testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq, INT32_MAX})), testing::_ ) ) .WillOnce(Return(transCursor)); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_index_min": {}, "ledger_index_max": {}, "binary": true }})JSON", kNftId, -1, -1 ) ); auto const output = handler.process(kInput, Context{.yield = yield, .apiVersion = kApiVersion}); ASSERT_TRUE(output); EXPECT_EQ(output.result->at("nft_id").as_string(), kNftId); EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq); EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq); EXPECT_EQ( output.result->at("marker").as_object(), boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") ); EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); EXPECT_EQ( output.result->at("transactions").as_array()[0].as_object().at("meta_blob").as_string(), "201C00000000F8E5110061E762400000000000001681144B4E9C06F24296074F7B" "C48F92A97916C6DC5EA9E1E1E5110061E76240000000000000178114D31252CF90" "2EF8DD8451243869B38667CBD89DF3E1E1F1031000" ); EXPECT_EQ( output.result->at("transactions").as_array()[0].as_object().at("tx_blob").as_string(), "120000240000002061400000000000000168400000000000000173047465737481" "144B4E9C06F24296074F7BC48F92A97916C6DC5EA98314D31252CF902EF8DD8451" "243869B38667CBD89DF3" ); EXPECT_EQ( output.result->at("transactions").as_array()[0].as_object().at("date").as_uint64(), 1 ); EXPECT_FALSE(output.result->as_object().contains("limit")); }); } TEST_F(RPCNFTHistoryHandlerTest, LimitAndMarker) { auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; ON_CALL(*backend_, fetchNFTTransactions).WillByDefault(Return(transCursor)); EXPECT_CALL( *backend_, fetchNFTTransactions( testing::_, testing::_, false, testing::Optional(testing::Eq(TransactionsCursor{10, 11})), testing::_ ) ) .Times(1); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_index_min": {}, "ledger_index_max": {}, "limit": 2, "forward": false, "marker": {{"ledger": 10, "seq": 11}} }})JSON", kNftId, -1, -1 ) ); auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->at("nft_id").as_string(), kNftId); EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq); EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq); EXPECT_EQ(output.result->at("limit").as_uint64(), 2); EXPECT_EQ( output.result->at("marker").as_object(), boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") ); EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); }); } TEST_F(RPCNFTHistoryHandlerTest, SpecificLedgerIndex) { // adjust the order for forward->false auto const transactions = genTransactions(kMaxSeq - 1, kMinSeq + 1); auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; ON_CALL(*backend_, fetchNFTTransactions).WillByDefault(Return(transCursor)); EXPECT_CALL( *backend_, fetchNFTTransactions( testing::_, testing::_, false, testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq - 1, INT32_MAX})), testing::_ ) ) .Times(1); auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq - 1); EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); ON_CALL(*backend_, fetchLedgerBySequence(kMaxSeq - 1, _)).WillByDefault(Return(ledgerHeader)); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_index": {} }})JSON", kNftId, kMaxSeq - 1 ) ); auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->at("nft_id").as_string(), kNftId); EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMaxSeq - 1); EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq - 1); EXPECT_FALSE(output.result->as_object().contains("limit")); EXPECT_FALSE(output.result->as_object().contains("marker")); EXPECT_EQ(output.result->at("transactions").as_array().size(), 1); }); } TEST_F(RPCNFTHistoryHandlerTest, SpecificNonexistLedgerIntIndex) { EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); ON_CALL(*backend_, fetchLedgerBySequence(kMaxSeq - 1, _)).WillByDefault(Return(std::nullopt)); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_index": {} }})JSON", kNftId, kMaxSeq - 1 ) ); auto const output = handler.process(kInput, Context{yield}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); }); } TEST_F(RPCNFTHistoryHandlerTest, SpecificNonexistLedgerStringIndex) { EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1); ON_CALL(*backend_, fetchLedgerBySequence(kMaxSeq - 1, _)).WillByDefault(Return(std::nullopt)); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_index": "{}" }})JSON", kNftId, kMaxSeq - 1 ) ); auto const output = handler.process(kInput, Context{yield}); ASSERT_FALSE(output); auto const err = rpc::makeError(output.result.error()); EXPECT_EQ(err.at("error").as_string(), "lgrNotFound"); EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound"); }); } TEST_F(RPCNFTHistoryHandlerTest, SpecificLedgerHash) { // adjust the order for forward->false auto const transactions = genTransactions(kMaxSeq - 1, kMinSeq + 1); auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; ON_CALL(*backend_, fetchNFTTransactions).WillByDefault(Return(transCursor)); EXPECT_CALL( *backend_, fetchNFTTransactions( testing::_, testing::_, false, testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq - 1, INT32_MAX})), testing::_ ) ) .Times(1); auto const ledgerHeader = createLedgerHeader(kLedgerHash, kMaxSeq - 1); EXPECT_CALL(*backend_, fetchLedgerByHash).Times(1); ON_CALL(*backend_, fetchLedgerByHash(xrpl::uint256{kLedgerHash}, _)) .WillByDefault(Return(ledgerHeader)); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_hash": "{}" }})JSON", kNftId, kLedgerHash ) ); auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->at("nft_id").as_string(), kNftId); EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMaxSeq - 1); EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq - 1); EXPECT_FALSE(output.result->as_object().contains("limit")); EXPECT_FALSE(output.result->as_object().contains("marker")); EXPECT_EQ(output.result->at("transactions").as_array().size(), 1); }); } TEST_F(RPCNFTHistoryHandlerTest, TxLessThanMinSeq) { auto const transactions = genTransactions(kMaxSeq - 1, kMinSeq + 1); auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; ON_CALL(*backend_, fetchNFTTransactions).WillByDefault(Return(transCursor)); EXPECT_CALL( *backend_, fetchNFTTransactions( testing::_, testing::_, false, testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq - 1, INT32_MAX})), testing::_ ) ) .Times(1); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_index_min": {}, "ledger_index_max": {}, "forward": false }})JSON", kNftId, kMinSeq + 2, kMaxSeq - 1 ) ); auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->at("nft_id").as_string(), kNftId); EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq + 2); EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq - 1); EXPECT_EQ(output.result->at("transactions").as_array().size(), 1); EXPECT_FALSE(output.result->as_object().contains("limit")); EXPECT_FALSE(output.result->as_object().contains("marker")); }); } TEST_F(RPCNFTHistoryHandlerTest, TxLargerThanMaxSeq) { auto const transactions = genTransactions(kMaxSeq - 1, kMinSeq + 1); auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; ON_CALL(*backend_, fetchNFTTransactions).WillByDefault(Return(transCursor)); EXPECT_CALL( *backend_, fetchNFTTransactions( testing::_, testing::_, false, testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq - 2, INT32_MAX})), testing::_ ) ) .Times(1); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_index_min": {}, "ledger_index_max": {}, "forward": false }})JSON", kNftId, kMinSeq + 1, kMaxSeq - 2 ) ); auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->at("nft_id").as_string(), kNftId); EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq + 1); EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq - 2); EXPECT_EQ(output.result->at("transactions").as_array().size(), 1); EXPECT_FALSE(output.result->as_object().contains("limit")); EXPECT_EQ( output.result->at("marker").as_object(), boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") ); }); } TEST_F(RPCNFTHistoryHandlerTest, LimitMoreThanMax) { auto const transactions = genTransactions(kMinSeq + 1, kMaxSeq - 1); auto const transCursor = TransactionsAndCursor{.txns = transactions, .cursor = TransactionsCursor{12, 34}}; ON_CALL(*backend_, fetchNFTTransactions).WillByDefault(Return(transCursor)); EXPECT_CALL( *backend_, fetchNFTTransactions( testing::_, testing::_, false, testing::Optional(testing::Eq(TransactionsCursor{kMaxSeq - 1, INT32_MAX})), testing::_ ) ) .Times(1); runSpawn([&, this](auto yield) { auto const handler = AnyHandler{NFTHistoryHandler{backend_}}; static auto const kInput = boost::json::parse( fmt::format( R"JSON({{ "nft_id": "{}", "ledger_index_min": {}, "ledger_index_max": {}, "forward": false, "limit": {} }})JSON", kNftId, kMinSeq + 1, kMaxSeq - 1, NFTHistoryHandler::kLimitMax + 1 ) ); auto const output = handler.process(kInput, Context{yield}); ASSERT_TRUE(output); EXPECT_EQ(output.result->at("nft_id").as_string(), kNftId); EXPECT_EQ(output.result->at("ledger_index_min").as_uint64(), kMinSeq + 1); EXPECT_EQ(output.result->at("ledger_index_max").as_uint64(), kMaxSeq - 1); EXPECT_EQ( output.result->at("marker").as_object(), boost::json::parse(R"JSON({"ledger": 12, "seq": 34})JSON") ); EXPECT_EQ(output.result->at("transactions").as_array().size(), 2); EXPECT_EQ(output.result->as_object().at("limit").as_uint64(), NFTHistoryHandler::kLimitMax); }); }