Add Unittests for subscription module (#488)

Fix #492
This commit is contained in:
cyan317
2023-02-03 09:07:02 +00:00
committed by GitHub
parent 1186622e58
commit 19455b4d6c
11 changed files with 1858 additions and 107 deletions

View File

@@ -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

View File

@@ -21,57 +21,6 @@
#include <subscriptions/SubscriptionManager.h>
#include <webserver/WsBase.h>
template <class T>
inline void
sendToSubscribers(
std::shared_ptr<Message> 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 <class T>
inline void
addSession(
std::shared_ptr<WsBase> session,
T& subscribers,
std::atomic_uint64_t& counter)
{
if (!subscribers.contains(session))
{
subscribers.insert(session);
++counter;
}
}
template <class T>
inline void
removeSession(
std::shared_ptr<WsBase> session,
T& subscribers,
std::atomic_uint64_t& counter)
{
if (subscribers.contains(session))
{
subscribers.erase(session);
--counter;
}
}
void
Subscription::subscribe(std::shared_ptr<WsBase> const& session)
{
@@ -96,55 +45,6 @@ Subscription::publish(std::shared_ptr<Message> const& message)
});
}
template <class Key>
void
SubscriptionMap<Key>::subscribe(
std::shared_ptr<WsBase> const& session,
Key const& account)
{
boost::asio::post(strand_, [this, session, account]() {
addSession(session, subscribers_[account], subCount_);
});
}
template <class Key>
void
SubscriptionMap<Key>::unsubscribe(
std::shared_ptr<WsBase> 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 <class Key>
void
SubscriptionMap<Key>::publish(
std::shared_ptr<Message>& 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<Backend::TransactionAndMetadata> const& transactions)
{
if (bookChangesSubscribers_.empty())
return;
auto const json = RPC::computeBookChanges(lgrInfo, transactions);
auto const bookChangesMsg =
std::make_shared<Message>(boost::json::serialize(json));

View File

@@ -95,7 +95,7 @@ public:
unsubscribe(std::shared_ptr<WsBase> const& session, Key const& key);
void
publish(std::shared_ptr<Message>& message, Key const& key);
publish(std::shared_ptr<Message> const& message, Key const& key);
std::uint64_t
count()
@@ -104,6 +104,106 @@ public:
}
};
template <class T>
inline void
sendToSubscribers(
std::shared_ptr<Message> 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 <class T>
inline void
addSession(
std::shared_ptr<WsBase> session,
T& subscribers,
std::atomic_uint64_t& counter)
{
if (!subscribers.contains(session))
{
subscribers.insert(session);
++counter;
}
}
template <class T>
inline void
removeSession(
std::shared_ptr<WsBase> session,
T& subscribers,
std::atomic_uint64_t& counter)
{
if (subscribers.contains(session))
{
subscribers.erase(session);
--counter;
}
}
template <class Key>
void
SubscriptionMap<Key>::subscribe(
std::shared_ptr<WsBase> const& session,
Key const& account)
{
boost::asio::post(strand_, [this, session, account]() {
addSession(session, subscribers_[account], subCount_);
});
}
template <class Key>
void
SubscriptionMap<Key>::unsubscribe(
std::shared_ptr<WsBase> 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 <class Key>
void
SubscriptionMap<Key>::publish(
std::shared_ptr<Message> 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<WsBase>;

View File

@@ -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 <subscriptions/SubscriptionManager.h>
#include <util/Fixtures.h>
#include <util/MockBackend.h>
#include <util/MockWsBase.h>
#include <util/TestObject.h>
#include <webserver/WsBase.h>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
#include <chrono>
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<MockBackend>(cfg);
auto subManager =
SubscriptionManager::make_SubscriptionManager(cfg, backend);
EXPECT_EQ(subManager->report(), json::parse(ReportReturn));
}
void
CheckSubscriberMessage(
std::string out,
std::shared_ptr<WsBase> session,
int retry = 10)
{
auto sessionPtr = static_cast<MockSession*>(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<SubscriptionManager> subManagerPtr;
util::TagDecoratorFactory tagDecoratorFactory{cfg};
std::shared_ptr<WsBase> session;
void
SetUp() override
{
MockBackendTest::SetUp();
subManagerPtr =
SubscriptionManager::make_SubscriptionManager(cfg, mockBackendPtr);
session = std::make_shared<MockSession>(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<WsBase> session1 =
std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<WsBase> session2 =
std::make_shared<MockSession>(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<MockBackend*>(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<WsBase> sessionIdle =
std::make_shared<MockSession>(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<MockBackend*>(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<TransactionAndMetadata>{};
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<MockBackend*>(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<MockBackend*>(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<MockBackend*>(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<WsBase> session1 =
std::make_shared<MockSession>(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<WsBase> session2 =
std::make_shared<MockSession>(tagDecoratorFactory);
subManagerPtr->subBook(book, session2);
metaObj = CreateMetaDataForCreateOffer(CURRENCY, ISSUER, 22, 3, 1);
trans1.metadata = metaObj.getSerializer().peekData();
subManagerPtr->pubTransaction(trans1, ledgerinfo);
CheckSubscriberMessage(OrderbookCreatePublish, session2);
}

View File

@@ -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 <subscriptions/SubscriptionManager.h>
#include <util/Fixtures.h>
#include <util/MockWsBase.h>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
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<WsBase> session1 =
std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<WsBase> session2 =
std::make_shared<MockSession>(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<WsBase> session1 =
std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<WsBase> session2 =
std::make_shared<MockSession>(tagDecoratorFactory);
sub.subscribe(session1);
sub.subscribe(session2);
ctx.run();
EXPECT_EQ(sub.count(), 2);
sub.publish(std::make_shared<Message>("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<Message>("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<WsBase> session1(new MockDeadSession(tagDecoratorFactory));
sub.subscribe(session1);
ctx.run();
EXPECT_EQ(sub.count(), 1);
// trigger dead
sub.publish(std::make_shared<Message>("message"));
ctx.restart();
ctx.run();
EXPECT_EQ(session1->dead(), true);
sub.publish(std::make_shared<Message>("message"));
ctx.restart();
ctx.run();
EXPECT_EQ(sub.count(), 0);
}
TEST_F(SubscriptionMapTest, SubscriptionMapCount)
{
std::shared_ptr<WsBase> session1 =
std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<WsBase> session2 =
std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<WsBase> session3 =
std::make_shared<MockSession>(tagDecoratorFactory);
SubscriptionMap<std::string> 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<WsBase> session1 =
std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<WsBase> session2 =
std::make_shared<MockSession>(tagDecoratorFactory);
SubscriptionMap<std::string> 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<Message>(topic1Message.data());
subMap.publish(message1, topic1); // lvalue
subMap.publish(
std::make_shared<Message>(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<WsBase> session1(new MockDeadSession(tagDecoratorFactory));
std::shared_ptr<WsBase> session2 =
std::make_shared<MockSession>(tagDecoratorFactory);
SubscriptionMap<std::string> 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<Message>(topic1Message.data());
subMap.publish(message1, topic1); // lvalue
subMap.publish(
std::make_shared<Message>(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);
}

View File

@@ -30,7 +30,6 @@
#include <optional>
#include <string>
using namespace testing;
using namespace clio;
using namespace std;

View File

@@ -25,6 +25,7 @@
#include <boost/asio.hpp>
#include "MockBackend.h"
#include <gtest/gtest.h>
#include <log/Logger.h>
@@ -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<MockBackend>(cfg);
}
void
TearDown() override
{
mockBackendPtr.reset();
}
protected:
std::shared_ptr<BackendInterface> mockBackendPtr;
};

View File

@@ -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 <backend/BackendInterface.h>
#include <gmock/gmock.h>
using namespace Backend;
class MockBackend : public BackendInterface
{
public:
MockBackend(clio::Config cfg) : BackendInterface(cfg)
{
}
MOCK_METHOD(
std::optional<ripple::LedgerInfo>,
fetchLedgerBySequence,
(std::uint32_t const sequence, boost::asio::yield_context& yield),
(const, override));
MOCK_METHOD(
std::optional<ripple::LedgerInfo>,
fetchLedgerByHash,
(ripple::uint256 const& hash, boost::asio::yield_context& yield),
(const, override));
MOCK_METHOD(
std::optional<std::uint32_t>,
fetchLatestLedgerSequence,
(boost::asio::yield_context & yield),
(const, override));
MOCK_METHOD(
std::optional<TransactionAndMetadata>,
fetchTransaction,
(ripple::uint256 const& hash, boost::asio::yield_context& yield),
(const, override));
MOCK_METHOD(
std::vector<TransactionAndMetadata>,
fetchTransactions,
(std::vector<ripple::uint256> 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<TransactionsCursor> const& cursor,
boost::asio::yield_context& yield),
(const, override));
MOCK_METHOD(
std::vector<TransactionAndMetadata>,
fetchAllTransactionsInLedger,
(std::uint32_t const ledgerSequence, boost::asio::yield_context& yield),
(const, override));
MOCK_METHOD(
std::vector<ripple::uint256>,
fetchAllTransactionHashesInLedger,
(std::uint32_t const ledgerSequence, boost::asio::yield_context& yield),
(const, override));
MOCK_METHOD(
std::optional<NFT>,
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<TransactionsCursor> const& cursorIn,
boost::asio::yield_context& yield),
(const, override));
MOCK_METHOD(
std::vector<Blob>,
doFetchLedgerObjects,
(std::vector<ripple::uint256> const& key,
std::uint32_t const sequence,
boost::asio::yield_context& yield),
(const, override));
MOCK_METHOD(
std::optional<Blob>,
doFetchLedgerObject,
(ripple::uint256 const& key,
std::uint32_t const sequence,
boost::asio::yield_context& yield),
(const, override));
MOCK_METHOD(
std::vector<LedgerObject>,
fetchLedgerDiff,
(std::uint32_t const ledgerSequence, boost::asio::yield_context& yield),
(const, override));
MOCK_METHOD(
std::optional<ripple::uint256>,
doFetchSuccessorKey,
(ripple::uint256 key,
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield),
(const, override));
MOCK_METHOD(
std::optional<LedgerRange>,
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<NFTsData> && blob), (override));
MOCK_METHOD(
void,
writeAccountTransactions,
(std::vector<AccountTransactionsData> && blob),
(override));
MOCK_METHOD(
void,
writeNFTTransactions,
(std::vector<NFTTransactionsData> && 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));
};

View File

@@ -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 <webserver/WsBase.h>
struct MockSession : public WsBase
{
std::string message;
void
send(std::shared_ptr<Message> 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<Message> 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)
{
}
};

View File

@@ -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 <ripple/protocol/STArray.h>
#include <ripple/protocol/TER.h>
ripple::AccountID
GetAccountIDWithString(std::string_view id)
{
return ripple::parseBase58<ripple::AccountID>(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<ripple::AccountID>(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<ripple::AccountID>(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<ripple::AccountID>(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<ripple::AccountID>(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<ripple::AccountID>(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;
}

137
unittests/util/TestObject.h Normal file
View File

@@ -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 <ripple/ledger/ReadView.h>
#include <string_view>
/*
* 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);