diff --git a/CMakeLists.txt b/CMakeLists.txt index f8ba0525..6836da9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,6 +106,9 @@ if(BUILD_TESTS) unittests/Config.cpp unittests/ProfilerTest.cpp unittests/DOSGuard.cpp + unittests/SubscriptionTest.cpp + unittests/SubscriptionManagerTest.cpp + unittests/util/TestObject.cpp unittests/rpc/ErrorTests.cpp unittests/rpc/BaseTests.cpp unittests/rpc/handlers/TestHandlerTests.cpp diff --git a/src/subscriptions/SubscriptionManager.cpp b/src/subscriptions/SubscriptionManager.cpp index 11a3fe63..273faafd 100644 --- a/src/subscriptions/SubscriptionManager.cpp +++ b/src/subscriptions/SubscriptionManager.cpp @@ -21,57 +21,6 @@ #include #include -template -inline void -sendToSubscribers( - std::shared_ptr const& message, - T& subscribers, - std::atomic_uint64_t& counter) -{ - for (auto it = subscribers.begin(); it != subscribers.end();) - { - auto& session = *it; - if (session->dead()) - { - it = subscribers.erase(it); - --counter; - } - else - { - session->send(message); - ++it; - } - } -} - -template -inline void -addSession( - std::shared_ptr session, - T& subscribers, - std::atomic_uint64_t& counter) -{ - if (!subscribers.contains(session)) - { - subscribers.insert(session); - ++counter; - } -} - -template -inline void -removeSession( - std::shared_ptr session, - T& subscribers, - std::atomic_uint64_t& counter) -{ - if (subscribers.contains(session)) - { - subscribers.erase(session); - --counter; - } -} - void Subscription::subscribe(std::shared_ptr const& session) { @@ -96,55 +45,6 @@ Subscription::publish(std::shared_ptr const& message) }); } -template -void -SubscriptionMap::subscribe( - std::shared_ptr const& session, - Key const& account) -{ - boost::asio::post(strand_, [this, session, account]() { - addSession(session, subscribers_[account], subCount_); - }); -} - -template -void -SubscriptionMap::unsubscribe( - std::shared_ptr const& session, - Key const& account) -{ - boost::asio::post(strand_, [this, account, session]() { - if (!subscribers_.contains(account)) - return; - - if (!subscribers_[account].contains(session)) - return; - - --subCount_; - - subscribers_[account].erase(session); - - if (subscribers_[account].size() == 0) - { - subscribers_.erase(account); - } - }); -} - -template -void -SubscriptionMap::publish( - std::shared_ptr& message, - Key const& account) -{ - boost::asio::post(strand_, [this, account, message]() { - if (!subscribers_.contains(account)) - return; - - sendToSubscribers(message, subscribers_[account], subCount_); - }); -} - boost::json::object getLedgerPubMessage( ripple::LedgerInfo const& lgrInfo, @@ -345,8 +245,6 @@ SubscriptionManager::pubTransaction( for (auto const& node : meta->getNodes()) { - if (!node.isFieldPresent(ripple::sfLedgerEntryType)) - assert(false); if (node.getFieldU16(ripple::sfLedgerEntryType) == ripple::ltOFFER) { ripple::SField const* field = nullptr; @@ -388,9 +286,6 @@ SubscriptionManager::pubBookChanges( ripple::LedgerInfo const& lgrInfo, std::vector const& transactions) { - if (bookChangesSubscribers_.empty()) - return; - auto const json = RPC::computeBookChanges(lgrInfo, transactions); auto const bookChangesMsg = std::make_shared(boost::json::serialize(json)); diff --git a/src/subscriptions/SubscriptionManager.h b/src/subscriptions/SubscriptionManager.h index d6d87886..70ef3d7b 100644 --- a/src/subscriptions/SubscriptionManager.h +++ b/src/subscriptions/SubscriptionManager.h @@ -95,7 +95,7 @@ public: unsubscribe(std::shared_ptr const& session, Key const& key); void - publish(std::shared_ptr& message, Key const& key); + publish(std::shared_ptr const& message, Key const& key); std::uint64_t count() @@ -104,6 +104,106 @@ public: } }; +template +inline void +sendToSubscribers( + std::shared_ptr const& message, + T& subscribers, + std::atomic_uint64_t& counter) +{ + for (auto it = subscribers.begin(); it != subscribers.end();) + { + auto& session = *it; + if (session->dead()) + { + it = subscribers.erase(it); + --counter; + } + else + { + session->send(message); + ++it; + } + } +} + +template +inline void +addSession( + std::shared_ptr session, + T& subscribers, + std::atomic_uint64_t& counter) +{ + if (!subscribers.contains(session)) + { + subscribers.insert(session); + ++counter; + } +} + +template +inline void +removeSession( + std::shared_ptr session, + T& subscribers, + std::atomic_uint64_t& counter) +{ + if (subscribers.contains(session)) + { + subscribers.erase(session); + --counter; + } +} + +template +void +SubscriptionMap::subscribe( + std::shared_ptr const& session, + Key const& account) +{ + boost::asio::post(strand_, [this, session, account]() { + addSession(session, subscribers_[account], subCount_); + }); +} + +template +void +SubscriptionMap::unsubscribe( + std::shared_ptr const& session, + Key const& account) +{ + boost::asio::post(strand_, [this, account, session]() { + if (!subscribers_.contains(account)) + return; + + if (!subscribers_[account].contains(session)) + return; + + --subCount_; + + subscribers_[account].erase(session); + + if (subscribers_[account].size() == 0) + { + subscribers_.erase(account); + } + }); +} + +template +void +SubscriptionMap::publish( + std::shared_ptr const& message, + Key const& account) +{ + boost::asio::post(strand_, [this, account, message]() { + if (!subscribers_.contains(account)) + return; + + sendToSubscribers(message, subscribers_[account], subCount_); + }); +} + class SubscriptionManager { using session_ptr = std::shared_ptr; diff --git a/unittests/SubscriptionManagerTest.cpp b/unittests/SubscriptionManagerTest.cpp new file mode 100644 index 00000000..b65c24a1 --- /dev/null +++ b/unittests/SubscriptionManagerTest.cpp @@ -0,0 +1,880 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include + +#include +#include + +#include + +using namespace std::chrono_literals; +namespace json = boost::json; +using namespace Backend; +using ::testing::Return; + +// common const +constexpr static auto CURRENCY = "0158415500000000C1F76FF6ECB0BAC600000000"; +constexpr static auto ISSUER = "rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD"; +constexpr static auto ACCOUNT1 = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"; +constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"; +constexpr static auto LEDGERHASH = + "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; +constexpr static auto LEDGERHASH2 = + "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; +constexpr static auto TXNID = + "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321"; + +/* + * test subscription factory method and report function + */ +TEST(SubscriptionManagerTest, InitAndReport) +{ + constexpr static auto ReportReturn = R"({ + "ledger":0, + "transactions":0, + "transactions_proposed":0, + "manifests":0, + "validations":0, + "account":0, + "accounts_proposed":0, + "books":0, + "book_changes":0 + })"; + clio::Config cfg; + auto backend = std::make_shared(cfg); + auto subManager = + SubscriptionManager::make_SubscriptionManager(cfg, backend); + EXPECT_EQ(subManager->report(), json::parse(ReportReturn)); +} + +void +CheckSubscriberMessage( + std::string out, + std::shared_ptr session, + int retry = 10) +{ + auto sessionPtr = static_cast(session.get()); + while (retry-- != 0) + { + std::this_thread::sleep_for(20ms); + if ((!sessionPtr->message.empty()) && + json::parse(sessionPtr->message) == json::parse(out)) + { + return; + } + } + EXPECT_TRUE(false) << "Could not wait the subscriber message, expect:" + << out << " Get:" << sessionPtr->message; +} + +// Fixture contains test target and mock backend +class SubscriptionManagerSimpleBackendTest : public MockBackendTest +{ +protected: + clio::Config cfg; + std::shared_ptr subManagerPtr; + util::TagDecoratorFactory tagDecoratorFactory{cfg}; + std::shared_ptr session; + void + SetUp() override + { + MockBackendTest::SetUp(); + subManagerPtr = + SubscriptionManager::make_SubscriptionManager(cfg, mockBackendPtr); + session = std::make_shared(tagDecoratorFactory); + } + void + TearDown() override + { + MockBackendTest::TearDown(); + subManagerPtr.reset(); + } +}; + +/* + * test report function and unsub functions + */ +TEST_F(SubscriptionManagerSimpleBackendTest, ReportCurrentSubscriber) +{ + constexpr static auto ReportReturn = R"({ + "ledger":0, + "transactions":2, + "transactions_proposed":2, + "manifests":2, + "validations":2, + "account":2, + "accounts_proposed":2, + "books":2, + "book_changes":2 + })"; + std::shared_ptr session1 = + std::make_shared(tagDecoratorFactory); + std::shared_ptr session2 = + std::make_shared(tagDecoratorFactory); + subManagerPtr->subBookChanges(session1); + subManagerPtr->subBookChanges(session2); + subManagerPtr->subManifest(session1); + subManagerPtr->subManifest(session2); + subManagerPtr->subProposedTransactions(session1); + subManagerPtr->subProposedTransactions(session2); + subManagerPtr->subTransactions(session1); + subManagerPtr->subTransactions(session2); + subManagerPtr->subValidation(session1); + subManagerPtr->subValidation(session2); + auto account = GetAccountIDWithString(ACCOUNT1); + subManagerPtr->subAccount(account, session1); + subManagerPtr->subAccount(account, session2); + subManagerPtr->subProposedAccount(account, session1); + subManagerPtr->subProposedAccount(account, session2); + auto issue1 = GetIssue(CURRENCY, ISSUER); + ripple::Book book{ripple::xrpIssue(), issue1}; + subManagerPtr->subBook(book, session1); + subManagerPtr->subBook(book, session2); + std::this_thread::sleep_for(20ms); + EXPECT_EQ(subManagerPtr->report(), json::parse(ReportReturn)); + subManagerPtr->unsubBookChanges(session1); + subManagerPtr->unsubManifest(session1); + subManagerPtr->unsubProposedTransactions(session1); + subManagerPtr->unsubTransactions(session1); + subManagerPtr->unsubValidation(session1); + subManagerPtr->unsubAccount(account, session1); + subManagerPtr->unsubProposedAccount(account, session1); + subManagerPtr->unsubBook(book, session1); + std::this_thread::sleep_for(20ms); + auto checkResult = [](json::object reportReturn, int result) { + EXPECT_EQ(reportReturn["book_changes"], result); + EXPECT_EQ(reportReturn["validations"], result); + EXPECT_EQ(reportReturn["transactions_proposed"], result); + EXPECT_EQ(reportReturn["transactions"], result); + EXPECT_EQ(reportReturn["manifests"], result); + EXPECT_EQ(reportReturn["accounts_proposed"], result); + EXPECT_EQ(reportReturn["account"], result); + EXPECT_EQ(reportReturn["books"], result); + }; + checkResult(subManagerPtr->report(), 1); + subManagerPtr->cleanup(session2); + subManagerPtr->cleanup(session2); // clean a removed session + std::this_thread::sleep_for(20ms); + checkResult(subManagerPtr->report(), 0); +} + +TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerLedgerUnSub) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + boost::asio::io_context ctx; + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + // mock fetchLedgerBySequence return this ledger + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // mock doFetchLedgerObject return fee setting ledger object + auto feeBlob = CreateFeeSettingBlob(1, 2, 3, 4, 0); + ON_CALL(*rawBackendPtr, doFetchLedgerObject).WillByDefault(Return(feeBlob)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + subManagerPtr->subLedger(yield, session); + }); + ctx.run(); + std::this_thread::sleep_for(20ms); + auto report = subManagerPtr->report(); + EXPECT_EQ(report["ledger"], 1); + subManagerPtr->cleanup(session); + subManagerPtr->unsubLedger(session); + std::this_thread::sleep_for(20ms); + report = subManagerPtr->report(); + EXPECT_EQ(report["ledger"], 0); +} + +/* + * test Manifest + * Subscription Manager forward the manifest message to subscribers + */ +TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerManifestTest) +{ + subManagerPtr->subManifest(session); + constexpr static auto dummyManifest = R"({"manifest":"test"})"; + subManagerPtr->forwardManifest(json::parse(dummyManifest).get_object()); + CheckSubscriberMessage(dummyManifest, session); +} + +/* + * test Validation + * Subscription Manager forward the validation message to subscribers + */ +TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerValidation) +{ + subManagerPtr->subValidation(session); + constexpr static auto dummyValidation = R"({"validation":"test"})"; + subManagerPtr->forwardValidation(json::parse(dummyValidation).get_object()); + CheckSubscriberMessage(dummyValidation, session); +} + +/* + * test ProposedTransaction + * We don't need the valid transaction in this test, subscription manager just + * forward the message to subscriber + */ +TEST_F( + SubscriptionManagerSimpleBackendTest, + SubscriptionManagerProposedTransaction) +{ + subManagerPtr->subProposedTransactions(session); + constexpr static auto dummyTransaction = R"({ + "transaction": + { + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + } + })"; + subManagerPtr->forwardProposedTransaction( + json::parse(dummyTransaction).get_object()); + CheckSubscriberMessage(dummyTransaction, session); +} + +/* + * test ProposedTransaction for one account + * we need to construct a valid account in the transaction + * this test subscribe the proposed transaction for two accounts + * but only forward a transaction with one of them + * check the correct session is called + */ +TEST_F( + SubscriptionManagerSimpleBackendTest, + SubscriptionManagerAccountProposedTransaction) +{ + auto account = GetAccountIDWithString(ACCOUNT1); + subManagerPtr->subProposedAccount(account, session); + + std::shared_ptr sessionIdle = + std::make_shared(tagDecoratorFactory); + auto accountIdle = GetAccountIDWithString(ACCOUNT2); + subManagerPtr->subProposedAccount(accountIdle, sessionIdle); + + constexpr static auto dummyTransaction = R"({ + "transaction": + { + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + } + })"; + subManagerPtr->forwardProposedTransaction( + json::parse(dummyTransaction).get_object()); + CheckSubscriberMessage(dummyTransaction, session); + auto rawIdle = (MockSession*)(sessionIdle.get()); + EXPECT_EQ("", rawIdle->message); +} + +/* + * test ledger stream + * check 1 subscribe response, 2 publish message + * mock backend to return fee ledger object + */ +TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerLedger) +{ + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + boost::asio::io_context ctx; + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + // mock fetchLedgerBySequence return this ledger + ON_CALL(*rawBackendPtr, fetchLedgerBySequence) + .WillByDefault(Return(ledgerinfo)); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + // mock doFetchLedgerObject return fee setting ledger object + auto feeBlob = CreateFeeSettingBlob(1, 2, 3, 4, 0); + ON_CALL(*rawBackendPtr, doFetchLedgerObject).WillByDefault(Return(feeBlob)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + // check the function response + // Information about the ledgers on hand and current fee schedule. This + // includes the same fields as a ledger stream message, except that it omits + // the type and txn_count fields + constexpr static auto LedgerResponse = R"({ + "validated_ledgers":"10-30", + "ledger_index":30, + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_time":0, + "fee_ref":4, + "fee_base":1, + "reserve_base":3, + "reserve_inc":2 + })"; + boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { + auto res = subManagerPtr->subLedger(yield, session); + // check the response + EXPECT_EQ(res, json::parse(LedgerResponse)); + }); + ctx.run(); + // test publish + auto ledgerinfo2 = CreateLedgerInfo(LEDGERHASH, 31); + auto fee2 = ripple::Fees(); + fee2.reserve = 10; + subManagerPtr->pubLedger(ledgerinfo2, fee2, "10-31", 8); + constexpr static auto LedgerPub = R"({ + "type":"ledgerClosed", + "ledger_index":31, + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_time":0, + "fee_ref":0, + "fee_base":0, + "reserve_base":10, + "reserve_inc":0, + "validated_ledgers":"10-31", + "txn_count":8 + })"; + CheckSubscriberMessage(LedgerPub, session); +} + +/* + * test book change + * create a book change meta data for + * XRP vs A token + * the transaction is just placeholder + * Book change computing only needs meta data + */ +TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerBookChange) +{ + subManagerPtr->subBookChanges(session); + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 32); + auto transactions = std::vector{}; + auto trans1 = TransactionAndMetadata(); + ripple::STObject obj = + CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + ripple::STObject metaObj = + CreateMetaDataForBookChange(CURRENCY, ISSUER, 22, 1, 3, 3, 1); + trans1.metadata = metaObj.getSerializer().peekData(); + transactions.push_back(trans1); + subManagerPtr->pubBookChanges(ledgerinfo, transactions); + constexpr static auto BookChangePublish = R"({ + "type":"bookChanges", + "ledger_index":32, + "ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652", + "ledger_time":0, + "changes":[ + { + "currency_a":"XRP_drops", + "currency_b":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD/0158415500000000C1F76FF6ECB0BAC600000000", + "volume_a":"2", + "volume_b":"2", + "high":"-1", + "low":"-1", + "open":"-1", + "close":"-1" + } + ] + })"; + CheckSubscriberMessage(BookChangePublish, session, 20); +} + +/* + * test transaction stream + */ +TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerTransaction) +{ + subManagerPtr->subTransactions(session); + + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH2, 33); + + auto trans1 = TransactionAndMetadata(); + ripple::STObject obj = + CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + // create an empty meta object + ripple::STArray metaArray{0}; + ripple::STObject metaObj(ripple::sfTransactionMetaData); + metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); + metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); + metaObj.setFieldU32(ripple::sfTransactionIndex, 22); + trans1.metadata = metaObj.getSerializer().peekData(); + subManagerPtr->pubTransaction(trans1, ledgerinfo); + constexpr static auto TransactionPublish = R"({ + "transaction":{ + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount":"1", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TransactionType":"Payment", + "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "date":0 + }, + "meta":{ + "AffectedNodes":[], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS", + "delivered_amount":"unavailable" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + CheckSubscriberMessage(TransactionPublish, session); +} + +/* + * test transaction for offer creation + * check owner_funds + * mock backend return a trustline + */ +TEST_F( + SubscriptionManagerSimpleBackendTest, + SubscriptionManagerTransactionOfferCreation) +{ + subManagerPtr->subTransactions(session); + + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH2, 33); + auto trans1 = TransactionAndMetadata(); + ripple::STObject obj = CreateCreateOfferTransactionObject( + ACCOUNT1, 1, 32, CURRENCY, ISSUER, 1, 3); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + ripple::STArray metaArray{0}; + ripple::STObject metaObj(ripple::sfTransactionMetaData); + metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); + metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); + metaObj.setFieldU32(ripple::sfTransactionIndex, 22); + trans1.metadata = metaObj.getSerializer().peekData(); + + ripple::STObject line(ripple::sfIndexes); + line.setFieldU16(ripple::sfLedgerEntryType, ripple::ltRIPPLE_STATE); + line.setFieldAmount(ripple::sfLowLimit, ripple::STAmount(10, false)); + line.setFieldAmount(ripple::sfHighLimit, ripple::STAmount(100, false)); + line.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{TXNID}); + line.setFieldU32(ripple::sfPreviousTxnLgrSeq, 3); + line.setFieldU32(ripple::sfFlags, 0); + auto issue2 = GetIssue(CURRENCY, ISSUER); + line.setFieldAmount(ripple::sfBalance, ripple::STAmount(issue2, 100)); + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + ON_CALL(*rawBackendPtr, doFetchLedgerObject) + .WillByDefault(Return(line.getSerializer().peekData())); + subManagerPtr->pubTransaction(trans1, ledgerinfo); + constexpr static auto TransactionForOwnerFund = R"({ + "transaction":{ + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TakerGets":{ + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"1" + }, + "TakerPays":"3", + "TransactionType":"OfferCreate", + "hash":"EE8775B43A67F4803DECEC5E918E0EA9C56D8ED93E512EBE9F2891846509AAAB", + "date":0, + "owner_funds":"100" + }, + "meta":{ + "AffectedNodes":[], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + CheckSubscriberMessage(TransactionForOwnerFund, session); +} + +constexpr static auto TransactionForOwnerFundFrozen = R"({ + "transaction":{ + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TakerGets":{ + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"1" + }, + "TakerPays":"3", + "TransactionType":"OfferCreate", + "hash":"EE8775B43A67F4803DECEC5E918E0EA9C56D8ED93E512EBE9F2891846509AAAB", + "date":0, + "owner_funds":"0" + }, + "meta":{ + "AffectedNodes":[], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." +})"; + +/* + * test transaction for offer creation + * check owner_funds when line is frozen + * mock backend return a trustline + */ +TEST_F( + SubscriptionManagerSimpleBackendTest, + SubscriptionManagerTransactionOfferCreationFrozenLine) +{ + subManagerPtr->subTransactions(session); + + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH2, 33); + auto trans1 = TransactionAndMetadata(); + ripple::STObject obj = CreateCreateOfferTransactionObject( + ACCOUNT1, 1, 32, CURRENCY, ISSUER, 1, 3); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + ripple::STArray metaArray{0}; + ripple::STObject metaObj(ripple::sfTransactionMetaData); + metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); + metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); + metaObj.setFieldU32(ripple::sfTransactionIndex, 22); + trans1.metadata = metaObj.getSerializer().peekData(); + + ripple::STObject line(ripple::sfIndexes); + line.setFieldU16(ripple::sfLedgerEntryType, ripple::ltRIPPLE_STATE); + line.setFieldAmount(ripple::sfLowLimit, ripple::STAmount(10, false)); + line.setFieldAmount(ripple::sfHighLimit, ripple::STAmount(100, false)); + line.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{TXNID}); + line.setFieldU32(ripple::sfPreviousTxnLgrSeq, 3); + line.setFieldU32(ripple::sfFlags, ripple::lsfHighFreeze); + line.setFieldAmount( + ripple::sfBalance, ripple::STAmount(GetIssue(CURRENCY, ISSUER), 100)); + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(3); + ON_CALL(*rawBackendPtr, doFetchLedgerObject) + .WillByDefault(Return(line.getSerializer().peekData())); + subManagerPtr->pubTransaction(trans1, ledgerinfo); + CheckSubscriberMessage(TransactionForOwnerFundFrozen, session); +} + +/* + * test transaction for offer creation + * check owner_funds when issue global frozen + * mock backend return a frozen account setting + */ +TEST_F( + SubscriptionManagerSimpleBackendTest, + SubscriptionManagerTransactionOfferCreationGlobalFrozen) +{ + subManagerPtr->subTransactions(session); + + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH2, 33); + auto trans1 = TransactionAndMetadata(); + ripple::STObject obj = CreateCreateOfferTransactionObject( + ACCOUNT1, 1, 32, CURRENCY, ISSUER, 1, 3); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + ripple::STArray metaArray{0}; + ripple::STObject metaObj(ripple::sfTransactionMetaData); + metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); + metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); + metaObj.setFieldU32(ripple::sfTransactionIndex, 22); + trans1.metadata = metaObj.getSerializer().peekData(); + + ripple::STObject line(ripple::sfIndexes); + line.setFieldU16(ripple::sfLedgerEntryType, ripple::ltRIPPLE_STATE); + line.setFieldAmount(ripple::sfLowLimit, ripple::STAmount(10, false)); + line.setFieldAmount(ripple::sfHighLimit, ripple::STAmount(100, false)); + line.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{TXNID}); + line.setFieldU32(ripple::sfPreviousTxnLgrSeq, 3); + line.setFieldU32(ripple::sfFlags, ripple::lsfHighFreeze); + auto issueAccount = GetAccountIDWithString(ISSUER); + line.setFieldAmount( + ripple::sfBalance, ripple::STAmount(GetIssue(CURRENCY, ISSUER), 100)); + MockBackend* rawBackendPtr = + static_cast(mockBackendPtr.get()); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + auto kk = ripple::keylet::account(issueAccount).key; + ON_CALL( + *rawBackendPtr, doFetchLedgerObject(testing::_, testing::_, testing::_)) + .WillByDefault(Return(line.getSerializer().peekData())); + ripple::STObject accountRoot = CreateAccountRootObject( + ISSUER, ripple::lsfGlobalFreeze, 1, 10, 2, TXNID, 3); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(kk, testing::_, testing::_)) + .WillByDefault(Return(accountRoot.getSerializer().peekData())); + subManagerPtr->pubTransaction(trans1, ledgerinfo); + CheckSubscriberMessage(TransactionForOwnerFundFrozen, session); +} + +/* + * test subscribe account + */ +TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerAccount) +{ + auto account = GetAccountIDWithString(ACCOUNT1); + subManagerPtr->subAccount(account, session); + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH2, 33); + + ripple::STObject obj = + CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + auto trans1 = TransactionAndMetadata(); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + ripple::STArray metaArray{1}; + ripple::STObject node(ripple::sfModifiedNode); + // emplace account into meta, trigger publish + ripple::STObject finalFields(ripple::sfFinalFields); + finalFields.setAccountID(ripple::sfAccount, account); + node.emplace_back(finalFields); + node.setFieldU16(ripple::sfLedgerEntryType, ripple::ltACCOUNT_ROOT); + metaArray.push_back(node); + ripple::STObject metaObj(ripple::sfTransactionMetaData); + metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); + metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); + metaObj.setFieldU32(ripple::sfTransactionIndex, 22); + trans1.metadata = metaObj.getSerializer().peekData(); + + subManagerPtr->pubTransaction(trans1, ledgerinfo); + constexpr static auto AccountPublish = R"({ + "transaction":{ + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount":"1", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TransactionType":"Payment", + "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "date":0 + }, + "meta":{ + "AffectedNodes":[ + { + "ModifiedNode":{ + "FinalFields":{ + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" + }, + "LedgerEntryType":"AccountRoot" + } + } + ], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS", + "delivered_amount":"unavailable" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + CheckSubscriberMessage(AccountPublish, session); +} + +/* + * test subscribe order book + * Create/Delete/Update offer node will trigger publish + */ +TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerOrderBook) +{ + auto issue1 = GetIssue(CURRENCY, ISSUER); + ripple::Book book{ripple::xrpIssue(), issue1}; + subManagerPtr->subBook(book, session); + auto ledgerinfo = CreateLedgerInfo(LEDGERHASH2, 33); + + auto trans1 = TransactionAndMetadata(); + auto obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32); + trans1.transaction = obj.getSerializer().peekData(); + trans1.ledgerSequence = 32; + + auto metaObj = + CreateMetaDataForBookChange(CURRENCY, ISSUER, 22, 3, 1, 1, 3); + trans1.metadata = metaObj.getSerializer().peekData(); + subManagerPtr->pubTransaction(trans1, ledgerinfo); + + constexpr static auto OrderbookPublish = R"({ + "transaction":{ + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount":"1", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TransactionType":"Payment", + "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "date":0 + }, + "meta":{ + "AffectedNodes":[ + { + "ModifiedNode":{ + "FinalFields":{ + "TakerGets":"3", + "TakerPays":{ + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"1" + } + }, + "LedgerEntryType":"Offer", + "PreviousFields":{ + "TakerGets":"1", + "TakerPays":{ + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"3" + } + } + } + } + ], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS", + "delivered_amount":"unavailable" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + CheckSubscriberMessage(OrderbookPublish, session); + + // trigger by offer cancel meta data + std::shared_ptr session1 = + std::make_shared(tagDecoratorFactory); + subManagerPtr->subBook(book, session1); + metaObj = CreateMetaDataForCancelOffer(CURRENCY, ISSUER, 22, 3, 1); + trans1.metadata = metaObj.getSerializer().peekData(); + subManagerPtr->pubTransaction(trans1, ledgerinfo); + constexpr static auto OrderbookCancelPublish = R"({ + "transaction":{ + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount":"1", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TransactionType":"Payment", + "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "date":0 + }, + "meta":{ + "AffectedNodes":[ + { + "DeletedNode":{ + "FinalFields":{ + "TakerGets":"3", + "TakerPays":{ + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"1" + } + }, + "LedgerEntryType":"Offer" + } + } + ], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS", + "delivered_amount":"unavailable" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + CheckSubscriberMessage(OrderbookCancelPublish, session1); + // trigger by offer create meta data + constexpr static auto OrderbookCreatePublish = R"({ + "transaction":{ + "Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount":"1", + "Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "Fee":"1", + "Sequence":32, + "SigningPubKey":"74657374", + "TransactionType":"Payment", + "hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2", + "date":0 + }, + "meta":{ + "AffectedNodes":[ + { + "CreatedNode":{ + "NewFields":{ + "TakerGets":"3", + "TakerPays":{ + "currency":"0158415500000000C1F76FF6ECB0BAC600000000", + "issuer":"rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD", + "value":"1" + } + }, + "LedgerEntryType":"Offer" + } + } + ], + "TransactionIndex":22, + "TransactionResult":"tesSUCCESS", + "delivered_amount":"unavailable" + }, + "type":"transaction", + "validated":true, + "status":"closed", + "ledger_index":33, + "ledger_hash":"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", + "engine_result_code":0, + "engine_result":"tesSUCCESS", + "engine_result_message":"The transaction was applied. Only final in a validated ledger." + })"; + std::shared_ptr session2 = + std::make_shared(tagDecoratorFactory); + subManagerPtr->subBook(book, session2); + metaObj = CreateMetaDataForCreateOffer(CURRENCY, ISSUER, 22, 3, 1); + trans1.metadata = metaObj.getSerializer().peekData(); + subManagerPtr->pubTransaction(trans1, ledgerinfo); + CheckSubscriberMessage(OrderbookCreatePublish, session2); +} diff --git a/unittests/SubscriptionTest.cpp b/unittests/SubscriptionTest.cpp new file mode 100644 index 00000000..5f3f3a1f --- /dev/null +++ b/unittests/SubscriptionTest.cpp @@ -0,0 +1,218 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace json = boost::json; + +TEST(MessageTest, Message) +{ + auto m = Message{"test"}; + EXPECT_STREQ(m.data(), "test"); + EXPECT_EQ(m.size(), 4); +} + +// io_context +class SubscriptionTest : public SyncAsioContextTest +{ +protected: + clio::Config cfg; + util::TagDecoratorFactory tagDecoratorFactory{cfg}; +}; + +class SubscriptionMapTest : public SubscriptionTest +{ +}; + +// subscribe/unsubscribe the same session would not change the count +TEST_F(SubscriptionTest, SubscriptionCount) +{ + Subscription sub(ctx); + std::shared_ptr session1 = + std::make_shared(tagDecoratorFactory); + std::shared_ptr session2 = + std::make_shared(tagDecoratorFactory); + sub.subscribe(session1); + sub.subscribe(session2); + ctx.run(); + EXPECT_EQ(sub.count(), 2); + sub.subscribe(session1); + ctx.restart(); + ctx.run(); + EXPECT_EQ(sub.count(), 2); + EXPECT_FALSE(sub.empty()); + sub.unsubscribe(session1); + ctx.restart(); + ctx.run(); + EXPECT_EQ(sub.count(), 1); + sub.unsubscribe(session1); + ctx.restart(); + ctx.run(); + EXPECT_EQ(sub.count(), 1); + sub.unsubscribe(session2); + ctx.restart(); + ctx.run(); + EXPECT_EQ(sub.count(), 0); + EXPECT_TRUE(sub.empty()); +} + +// send interface will be called when publish called +TEST_F(SubscriptionTest, SubscriptionPublish) +{ + Subscription sub(ctx); + std::shared_ptr session1 = + std::make_shared(tagDecoratorFactory); + std::shared_ptr session2 = + std::make_shared(tagDecoratorFactory); + sub.subscribe(session1); + sub.subscribe(session2); + ctx.run(); + EXPECT_EQ(sub.count(), 2); + sub.publish(std::make_shared("message")); + ctx.restart(); + ctx.run(); + MockSession* p1 = (MockSession*)(session1.get()); + EXPECT_EQ(p1->message, "message"); + MockSession* p2 = (MockSession*)(session2.get()); + EXPECT_EQ(p2->message, "message"); + sub.unsubscribe(session1); + ctx.restart(); + ctx.run(); + sub.publish(std::make_shared("message2")); + ctx.restart(); + ctx.run(); + EXPECT_EQ(p1->message, "message"); + EXPECT_EQ(p2->message, "messagemessage2"); +} + +// when error happen during send(), the subsciber will be removed after +TEST_F(SubscriptionTest, SubscriptionDeadRemoveSubscriber) +{ + Subscription sub(ctx); + std::shared_ptr session1(new MockDeadSession(tagDecoratorFactory)); + sub.subscribe(session1); + ctx.run(); + EXPECT_EQ(sub.count(), 1); + // trigger dead + sub.publish(std::make_shared("message")); + ctx.restart(); + ctx.run(); + EXPECT_EQ(session1->dead(), true); + sub.publish(std::make_shared("message")); + ctx.restart(); + ctx.run(); + EXPECT_EQ(sub.count(), 0); +} + +TEST_F(SubscriptionMapTest, SubscriptionMapCount) +{ + std::shared_ptr session1 = + std::make_shared(tagDecoratorFactory); + std::shared_ptr session2 = + std::make_shared(tagDecoratorFactory); + std::shared_ptr session3 = + std::make_shared(tagDecoratorFactory); + SubscriptionMap subMap(ctx); + subMap.subscribe(session1, "topic1"); + subMap.subscribe(session2, "topic1"); + subMap.subscribe(session3, "topic2"); + ctx.run(); + EXPECT_EQ(subMap.count(), 3); + subMap.subscribe(session1, "topic1"); + subMap.subscribe(session2, "topic1"); + ctx.restart(); + ctx.run(); + EXPECT_EQ(subMap.count(), 3); + subMap.unsubscribe(session1, "topic1"); + ctx.restart(); + ctx.run(); + subMap.unsubscribe(session1, "topic1"); + subMap.unsubscribe(session2, "topic1"); + subMap.unsubscribe(session3, "topic2"); + ctx.restart(); + ctx.run(); + EXPECT_EQ(subMap.count(), 0); + subMap.unsubscribe(session3, "topic2"); + subMap.unsubscribe(session3, "no exist"); + ctx.restart(); + ctx.run(); + EXPECT_EQ(subMap.count(), 0); +} + +TEST_F(SubscriptionMapTest, SubscriptionMapPublish) +{ + std::shared_ptr session1 = + std::make_shared(tagDecoratorFactory); + std::shared_ptr session2 = + std::make_shared(tagDecoratorFactory); + SubscriptionMap subMap(ctx); + const std::string topic1 = "topic1"; + const std::string topic2 = "topic2"; + const std::string topic1Message = "topic1Message"; + const std::string topic2Message = "topic2Message"; + subMap.subscribe(session1, topic1); + subMap.subscribe(session2, topic2); + ctx.run(); + EXPECT_EQ(subMap.count(), 2); + auto message1 = std::make_shared(topic1Message.data()); + subMap.publish(message1, topic1); // lvalue + subMap.publish( + std::make_shared(topic2Message.data()), topic2); // rvalue + ctx.restart(); + ctx.run(); + MockSession* p1 = (MockSession*)(session1.get()); + EXPECT_EQ(p1->message, topic1Message); + MockSession* p2 = (MockSession*)(session2.get()); + EXPECT_EQ(p2->message, topic2Message); +} + +TEST_F(SubscriptionMapTest, SubscriptionMapDeadRemoveSubscriber) +{ + std::shared_ptr session1(new MockDeadSession(tagDecoratorFactory)); + std::shared_ptr session2 = + std::make_shared(tagDecoratorFactory); + SubscriptionMap subMap(ctx); + const std::string topic1 = "topic1"; + const std::string topic2 = "topic2"; + const std::string topic1Message = "topic1Message"; + const std::string topic2Message = "topic2Message"; + subMap.subscribe(session1, topic1); + subMap.subscribe(session2, topic2); + ctx.run(); + EXPECT_EQ(subMap.count(), 2); + auto message1 = std::make_shared(topic1Message.data()); + subMap.publish(message1, topic1); // lvalue + subMap.publish( + std::make_shared(topic2Message.data()), topic2); // rvalue + ctx.restart(); + ctx.run(); + MockDeadSession* p1 = (MockDeadSession*)(session1.get()); + EXPECT_EQ(p1->dead(), true); + MockSession* p2 = (MockSession*)(session2.get()); + EXPECT_EQ(p2->message, topic2Message); + subMap.publish(message1, topic1); + ctx.restart(); + ctx.run(); + EXPECT_EQ(subMap.count(), 1); +} diff --git a/unittests/rpc/BaseTests.cpp b/unittests/rpc/BaseTests.cpp index 9d203d1b..a1865210 100644 --- a/unittests/rpc/BaseTests.cpp +++ b/unittests/rpc/BaseTests.cpp @@ -30,7 +30,6 @@ #include #include -using namespace testing; using namespace clio; using namespace std; diff --git a/unittests/util/Fixtures.h b/unittests/util/Fixtures.h index 53bfb8d0..d42a9b30 100644 --- a/unittests/util/Fixtures.h +++ b/unittests/util/Fixtures.h @@ -25,6 +25,7 @@ #include +#include "MockBackend.h" #include #include @@ -154,3 +155,25 @@ struct SyncAsioContextTest : public NoLoggerFixture protected: boost::asio::io_context ctx; }; + +/** + * @brief Fixture with an mock backend + */ +struct MockBackendTest : public NoLoggerFixture +{ + void + SetUp() override + { + NoLoggerFixture::SetUp(); + clio::Config cfg; + mockBackendPtr = std::make_shared(cfg); + } + void + TearDown() override + { + mockBackendPtr.reset(); + } + +protected: + std::shared_ptr mockBackendPtr; +}; diff --git a/unittests/util/MockBackend.h b/unittests/util/MockBackend.h new file mode 100644 index 00000000..415308c5 --- /dev/null +++ b/unittests/util/MockBackend.h @@ -0,0 +1,203 @@ +//------------------------------------------------------------------------------ +/* + 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 + +using namespace Backend; + +class MockBackend : public BackendInterface +{ +public: + MockBackend(clio::Config cfg) : BackendInterface(cfg) + { + } + MOCK_METHOD( + std::optional, + fetchLedgerBySequence, + (std::uint32_t const sequence, boost::asio::yield_context& yield), + (const, override)); + + MOCK_METHOD( + std::optional, + fetchLedgerByHash, + (ripple::uint256 const& hash, boost::asio::yield_context& yield), + (const, override)); + + MOCK_METHOD( + std::optional, + fetchLatestLedgerSequence, + (boost::asio::yield_context & yield), + (const, override)); + + MOCK_METHOD( + std::optional, + fetchTransaction, + (ripple::uint256 const& hash, boost::asio::yield_context& yield), + (const, override)); + + MOCK_METHOD( + std::vector, + fetchTransactions, + (std::vector const& hashes, + boost::asio::yield_context& yield), + (const, override)); + + MOCK_METHOD( + TransactionsAndCursor, + fetchAccountTransactions, + (ripple::AccountID const& account, + std::uint32_t const limit, + bool forward, + std::optional const& cursor, + boost::asio::yield_context& yield), + (const, override)); + + MOCK_METHOD( + std::vector, + fetchAllTransactionsInLedger, + (std::uint32_t const ledgerSequence, boost::asio::yield_context& yield), + (const, override)); + + MOCK_METHOD( + std::vector, + fetchAllTransactionHashesInLedger, + (std::uint32_t const ledgerSequence, boost::asio::yield_context& yield), + (const, override)); + + MOCK_METHOD( + std::optional, + fetchNFT, + (ripple::uint256 const& tokenID, + std::uint32_t const ledgerSequence, + boost::asio::yield_context& yieldd), + (const, override)); + + MOCK_METHOD( + TransactionsAndCursor, + fetchNFTTransactions, + (ripple::uint256 const& tokenID, + std::uint32_t const limit, + bool const forward, + std::optional const& cursorIn, + boost::asio::yield_context& yield), + (const, override)); + + MOCK_METHOD( + std::vector, + doFetchLedgerObjects, + (std::vector const& key, + std::uint32_t const sequence, + boost::asio::yield_context& yield), + (const, override)); + + MOCK_METHOD( + std::optional, + doFetchLedgerObject, + (ripple::uint256 const& key, + std::uint32_t const sequence, + boost::asio::yield_context& yield), + (const, override)); + + MOCK_METHOD( + std::vector, + fetchLedgerDiff, + (std::uint32_t const ledgerSequence, boost::asio::yield_context& yield), + (const, override)); + + MOCK_METHOD( + std::optional, + doFetchSuccessorKey, + (ripple::uint256 key, + std::uint32_t const ledgerSequence, + boost::asio::yield_context& yield), + (const, override)); + + MOCK_METHOD( + std::optional, + hardFetchLedgerRange, + (boost::asio::yield_context & yield), + (const, override)); + + MOCK_METHOD( + void, + writeLedger, + (ripple::LedgerInfo const& ledgerInfo, std::string&& ledgerHeader), + (override)); + + MOCK_METHOD( + void, + writeLedgerObject, + (std::string && key, std::uint32_t const seq, std::string&& blob), + (override)); + + MOCK_METHOD( + void, + writeTransaction, + (std::string && hash, + std::uint32_t const seq, + std::uint32_t const date, + std::string&& transaction, + std::string&& metadata), + (override)); + + MOCK_METHOD(void, writeNFTs, (std::vector && blob), (override)); + + MOCK_METHOD( + void, + writeAccountTransactions, + (std::vector && blob), + (override)); + + MOCK_METHOD( + void, + writeNFTTransactions, + (std::vector && blob), + (override)); + + MOCK_METHOD( + void, + writeSuccessor, + (std::string && key, std::uint32_t const seq, std::string&& successor), + (override)); + + MOCK_METHOD(void, startWrites, (), (const, override)); + + MOCK_METHOD( + bool, + doOnlineDelete, + (std::uint32_t numLedgersToKeep, boost::asio::yield_context& yield), + (const, override)); + + MOCK_METHOD(bool, isTooBusy, (), (const, override)); + + MOCK_METHOD(void, open, (bool), (override)); + + MOCK_METHOD(void, close, (), (override)); + + MOCK_METHOD( + void, + doWriteLedgerObject, + (std::string && key, std::uint32_t const seq, std::string&& blob), + (override)); + + MOCK_METHOD(bool, doFinishWrites, (), (override)); +}; diff --git a/unittests/util/MockWsBase.h b/unittests/util/MockWsBase.h new file mode 100644 index 00000000..5e0c86bd --- /dev/null +++ b/unittests/util/MockWsBase.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + 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 + +struct MockSession : public WsBase +{ + std::string message; + void + send(std::shared_ptr msg_type) override + { + message += std::string(msg_type->data()); + } + MockSession(util::TagDecoratorFactory const& factory) : WsBase(factory) + { + } +}; + +struct MockDeadSession : public WsBase +{ + void + send(std::shared_ptr msg_type) override + { + // err happen, the session should remove from subscribers + ec_.assign(2, boost::system::system_category()); + } + MockDeadSession(util::TagDecoratorFactory const& factory) : WsBase(factory) + { + } +}; diff --git a/unittests/util/TestObject.cpp b/unittests/util/TestObject.cpp new file mode 100644 index 00000000..56c8f666 --- /dev/null +++ b/unittests/util/TestObject.cpp @@ -0,0 +1,245 @@ +//------------------------------------------------------------------------------ +/* + 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 "TestObject.h" + +#include +#include + +ripple::AccountID +GetAccountIDWithString(std::string_view id) +{ + return ripple::parseBase58(std::string(id)).value(); +} + +ripple::LedgerInfo +CreateLedgerInfo(std::string_view ledgerHash, ripple::LedgerIndex seq) +{ + auto ledgerinfo = ripple::LedgerInfo(); + ledgerinfo.hash = ripple::uint256{ledgerHash}; + ledgerinfo.seq = seq; + return ledgerinfo; +} + +ripple::STObject +CreateFeeSettingLedgerObject( + uint64_t base, + uint32_t reserveInc, + uint32_t reserveBase, + uint32_t refFeeUnit, + uint32_t flag) +{ + ripple::STObject obj(ripple::sfFee); + obj.setFieldU16(ripple::sfLedgerEntryType, ripple::ltFEE_SETTINGS); + obj.setFieldU64(ripple::sfBaseFee, base); + obj.setFieldU32(ripple::sfReserveIncrement, reserveInc); + obj.setFieldU32(ripple::sfReserveBase, reserveBase); + obj.setFieldU32(ripple::sfReferenceFeeUnits, refFeeUnit); + obj.setFieldU32(ripple::sfFlags, flag); + return obj; +} + +ripple::Blob +CreateFeeSettingBlob( + uint64_t base, + uint32_t reserveInc, + uint32_t reserveBase, + uint32_t refFeeUnit, + uint32_t flag) +{ + auto lo = CreateFeeSettingLedgerObject( + base, reserveInc, reserveBase, refFeeUnit, flag); + return lo.getSerializer().peekData(); +} + +ripple::STObject +CreatePaymentTransactionObject( + std::string_view accountId1, + std::string_view accountId2, + int amount, + int fee, + uint32_t seq) +{ + ripple::STObject obj(ripple::sfTransaction); + obj.setFieldU16(ripple::sfTransactionType, ripple::ttPAYMENT); + auto account = + ripple::parseBase58(std::string(accountId1)); + obj.setAccountID(ripple::sfAccount, account.value()); + obj.setFieldAmount(ripple::sfAmount, ripple::STAmount(amount, false)); + obj.setFieldAmount(ripple::sfFee, ripple::STAmount(fee, false)); + auto account2 = + ripple::parseBase58(std::string(accountId2)); + obj.setAccountID(ripple::sfDestination, account2.value()); + obj.setFieldU32(ripple::sfSequence, seq); + const char* key = "test"; + ripple::Slice slice(key, 4); + obj.setFieldVL(ripple::sfSigningPubKey, slice); + return obj; +} + +ripple::STObject +CreateAccountRootObject( + std::string_view accountId, + uint32_t flag, + uint32_t seq, + int balance, + uint32_t ownerCount, + std::string_view previousTxnID, + uint32_t previousTxnSeq) +{ + ripple::STObject accountRoot(ripple::sfAccount); + accountRoot.setFieldU16(ripple::sfLedgerEntryType, ripple::ltACCOUNT_ROOT); + accountRoot.setFieldU32(ripple::sfFlags, flag); + accountRoot.setAccountID( + ripple::sfAccount, GetAccountIDWithString(accountId)); + accountRoot.setFieldU32(ripple::sfSequence, seq); + accountRoot.setFieldAmount( + ripple::sfBalance, ripple::STAmount(balance, false)); + accountRoot.setFieldU32(ripple::sfOwnerCount, ownerCount); + accountRoot.setFieldH256( + ripple::sfPreviousTxnID, ripple::uint256{previousTxnID}); + accountRoot.setFieldU32(ripple::sfPreviousTxnLgrSeq, previousTxnSeq); + return accountRoot; +} + +ripple::STObject +CreateCreateOfferTransactionObject( + std::string_view accountId, + int fee, + uint32_t seq, + std::string_view currency, + std::string_view issuer, + int takerGets, + int takerPays) +{ + ripple::STObject obj(ripple::sfTransaction); + obj.setFieldU16(ripple::sfTransactionType, ripple::ttOFFER_CREATE); + auto account = + ripple::parseBase58(std::string(accountId)); + obj.setAccountID(ripple::sfAccount, account.value()); + auto amount = ripple::STAmount(fee, false); + obj.setFieldAmount(ripple::sfFee, amount); + obj.setFieldU32(ripple::sfSequence, seq); + // add amount + ripple::Issue issue1( + ripple::Currency{currency}, + ripple::parseBase58(std::string(issuer)).value()); + obj.setFieldAmount( + ripple::sfTakerGets, ripple::STAmount(issue1, takerGets)); + obj.setFieldAmount(ripple::sfTakerPays, ripple::STAmount(takerPays, false)); + + auto key = "test"; + ripple::Slice slice(key, 4); + obj.setFieldVL(ripple::sfSigningPubKey, slice); + return obj; +} + +ripple::Issue +GetIssue(std::string_view currency, std::string_view issuerId) +{ + return ripple::Issue( + ripple::Currency{currency}, + ripple::parseBase58(std::string(issuerId)).value()); +} + +ripple::STObject +CreateMetaDataForBookChange( + std::string_view currency, + std::string_view issueId, + uint32_t transactionIndex, + int finalTakerGets, + int perviousTakerGets, + int finalTakerPays, + int perviousTakerPays) +{ + ripple::STObject finalFields(ripple::sfFinalFields); + ripple::Issue issue1 = GetIssue(currency, issueId); + finalFields.setFieldAmount( + ripple::sfTakerPays, ripple::STAmount(issue1, finalTakerPays)); + finalFields.setFieldAmount( + ripple::sfTakerGets, ripple::STAmount(finalTakerGets, false)); + ripple::STObject previousFields(ripple::sfPreviousFields); + previousFields.setFieldAmount( + ripple::sfTakerPays, ripple::STAmount(issue1, perviousTakerPays)); + previousFields.setFieldAmount( + ripple::sfTakerGets, ripple::STAmount(perviousTakerGets, false)); + ripple::STObject metaObj(ripple::sfTransactionMetaData); + ripple::STArray metaArray{1}; + ripple::STObject node(ripple::sfModifiedNode); + node.setFieldU16(ripple::sfLedgerEntryType, ripple::ltOFFER); + node.emplace_back(std::move(finalFields)); + node.emplace_back(std::move(previousFields)); + metaArray.push_back(node); + metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); + metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); + metaObj.setFieldU32(ripple::sfTransactionIndex, transactionIndex); + return metaObj; +} + +ripple::STObject +CreateMetaDataForCreateOffer( + std::string_view currency, + std::string_view issueId, + uint32_t transactionIndex, + int finalTakerGets, + int finalTakerPays) +{ + ripple::STObject finalFields(ripple::sfNewFields); + ripple::Issue issue1 = GetIssue(currency, issueId); + finalFields.setFieldAmount( + ripple::sfTakerPays, ripple::STAmount(issue1, finalTakerPays)); + finalFields.setFieldAmount( + ripple::sfTakerGets, ripple::STAmount(finalTakerGets, false)); + ripple::STObject metaObj(ripple::sfTransactionMetaData); + ripple::STArray metaArray{1}; + ripple::STObject node(ripple::sfCreatedNode); + node.setFieldU16(ripple::sfLedgerEntryType, ripple::ltOFFER); + node.emplace_back(std::move(finalFields)); + metaArray.push_back(node); + metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); + metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); + metaObj.setFieldU32(ripple::sfTransactionIndex, transactionIndex); + return metaObj; +} + +ripple::STObject +CreateMetaDataForCancelOffer( + std::string_view currency, + std::string_view issueId, + uint32_t transactionIndex, + int finalTakerGets, + int finalTakerPays) +{ + ripple::STObject finalFields(ripple::sfFinalFields); + ripple::Issue issue1 = GetIssue(currency, issueId); + finalFields.setFieldAmount( + ripple::sfTakerPays, ripple::STAmount(issue1, finalTakerPays)); + finalFields.setFieldAmount( + ripple::sfTakerGets, ripple::STAmount(finalTakerGets, false)); + ripple::STObject metaObj(ripple::sfTransactionMetaData); + ripple::STArray metaArray{1}; + ripple::STObject node(ripple::sfDeletedNode); + node.setFieldU16(ripple::sfLedgerEntryType, ripple::ltOFFER); + node.emplace_back(std::move(finalFields)); + metaArray.push_back(node); + metaObj.setFieldArray(ripple::sfAffectedNodes, metaArray); + metaObj.setFieldU8(ripple::sfTransactionResult, ripple::tesSUCCESS); + metaObj.setFieldU32(ripple::sfTransactionIndex, transactionIndex); + return metaObj; +} diff --git a/unittests/util/TestObject.h b/unittests/util/TestObject.h new file mode 100644 index 00000000..e8093c55 --- /dev/null +++ b/unittests/util/TestObject.h @@ -0,0 +1,137 @@ +//------------------------------------------------------------------------------ +/* + 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 + +/* + * Create AccountID object with string + */ +[[nodiscard]] ripple::AccountID +GetAccountIDWithString(std::string_view id); + +/* + * Create a simple ledgerInfo object with only hash and seq + */ +[[nodiscard]] ripple::LedgerInfo +CreateLedgerInfo(std::string_view ledgerHash, ripple::LedgerIndex seq); + +/* + * Create a FeeSetting ledger object + */ +[[nodiscard]] ripple::STObject +CreateFeeSettingLedgerObject( + uint64_t base, + uint32_t reserveInc, + uint32_t reserveBase, + uint32_t refFeeUnit, + uint32_t flag); + +/* + * Create a FeeSetting ledger object and return its blob + */ +[[nodiscard]] ripple::Blob +CreateFeeSettingBlob( + uint64_t base, + uint32_t reserveInc, + uint32_t reserveBase, + uint32_t refFeeUnit, + uint32_t flag); + +/* + * Create a payment transaction object + */ +[[nodiscard]] ripple::STObject +CreatePaymentTransactionObject( + std::string_view accountId1, + std::string_view accountId2, + int amount, + int fee, + uint32_t seq); + +/* + * Create an account root ledger object + */ +[[nodiscard]] ripple::STObject +CreateAccountRootObject( + std::string_view accountId, + uint32_t flag, + uint32_t seq, + int balance, + uint32_t ownerCount, + std::string_view previousTxnID, + uint32_t previousTxnSeq); + +/* + * Create a createoffer treansaction + * Taker pay is XRP + */ +[[nodiscard]] ripple::STObject +CreateCreateOfferTransactionObject( + std::string_view accountId, + int fee, + uint32_t seq, + std::string_view currency, + std::string_view issuer, + int takerGets, + int takerPays); + +/* + * Return an issue object with given currency and issue account + */ +[[nodiscard]] ripple::Issue +GetIssue(std::string_view currency, std::string_view issuerId); + +/* + * Create a offer change meta data + */ +[[nodiscard]] ripple::STObject +CreateMetaDataForBookChange( + std::string_view currency, + std::string_view issueId, + uint32_t transactionIndex, + int finalTakerGets, + int perviousTakerGets, + int finalTakerPays, + int perviousTakerPays); + +/* + * Meta data for adding a offer object + */ +[[nodiscard]] ripple::STObject +CreateMetaDataForCreateOffer( + std::string_view currency, + std::string_view issueId, + uint32_t transactionIndex, + int finalTakerGets, + int finalTakerPays); + +/* + * Meta data for removing a offer object + */ +[[nodiscard]] ripple::STObject +CreateMetaDataForCancelOffer( + std::string_view currency, + std::string_view issueId, + uint32_t transactionIndex, + int finalTakerGets, + int finalTakerPays);