New subscription manager (#1071)

Fix #886
This commit is contained in:
cyan317
2024-01-08 14:45:57 +00:00
committed by GitHub
parent 07bd4b0760
commit eb1831c489
37 changed files with 4427 additions and 2098 deletions

View File

@@ -1,870 +0,0 @@
//------------------------------------------------------------------------------
/*
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 "data/Types.h"
#include "feed/SubscriptionManager.h"
#include "util/Fixtures.h"
#include "util/MockBackend.h"
#include "util/MockWsBase.h"
#include "util/Taggable.h"
#include "util/TestObject.h"
#include "util/config/Config.h"
#include "web/interface/ConnectionBase.h"
#include <boost/asio/impl/spawn.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <ripple/basics/base_uint.h>
#include <ripple/protocol/Book.h>
#include <ripple/protocol/Fees.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/Issue.h>
#include <ripple/protocol/LedgerFormats.h>
#include <ripple/protocol/SField.h>
#include <ripple/protocol/STAmount.h>
#include <ripple/protocol/STArray.h>
#include <ripple/protocol/STObject.h>
#include <ripple/protocol/TER.h>
#include <chrono>
#include <memory>
#include <string>
#include <thread>
#include <vector>
using std::chrono::milliseconds;
namespace json = boost::json;
using namespace data;
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
})";
util::Config const cfg;
auto backend = std::make_shared<MockBackend>(cfg);
auto subManager = feed::SubscriptionManager::make_SubscriptionManager(cfg, backend);
EXPECT_EQ(subManager->report(), json::parse(ReportReturn));
}
void
CheckSubscriberMessage(std::string out, std::shared_ptr<web::ConnectionBase> session, int retry = 10)
{
auto sessionPtr = dynamic_cast<MockSession*>(session.get());
ASSERT_NE(sessionPtr, nullptr);
while (retry-- != 0) {
std::this_thread::sleep_for(milliseconds(20));
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:
util::Config cfg;
std::shared_ptr<feed::SubscriptionManager> subManagerPtr;
util::TagDecoratorFactory tagDecoratorFactory{cfg};
std::shared_ptr<web::ConnectionBase> session;
void
SetUp() override
{
MockBackendTest::SetUp();
subManagerPtr = feed::SubscriptionManager::make_SubscriptionManager(cfg, backend);
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<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<web::ConnectionBase> const 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 const book{ripple::xrpIssue(), issue1};
subManagerPtr->subBook(book, session1);
subManagerPtr->subBook(book, session2);
std::this_thread::sleep_for(milliseconds(20));
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(milliseconds(20));
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); // clean a removed session
std::this_thread::sleep_for(milliseconds(20));
checkResult(subManagerPtr->report(), 0);
}
TEST_F(SubscriptionManagerSimpleBackendTest, SubscriptionManagerLedgerUnSub)
{
backend->setRange(10, 30);
boost::asio::io_context ctx;
auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30);
// mock fetchLedgerBySequence return this ledger
ON_CALL(*backend, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo));
EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1);
// mock doFetchLedgerObject return fee setting ledger object
auto feeBlob = CreateFeeSettingBlob(1, 2, 3, 4, 0);
ON_CALL(*backend, doFetchLedgerObject).WillByDefault(Return(feeBlob));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(1);
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) { subManagerPtr->subLedger(yield, session); });
ctx.run();
std::this_thread::sleep_for(milliseconds(20));
auto report = subManagerPtr->report();
EXPECT_EQ(report["ledger"], 1);
subManagerPtr->cleanup(session);
subManagerPtr->unsubLedger(session);
std::this_thread::sleep_for(milliseconds(20));
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);
// transaction contains account and its public key
// make sure it is not parsed to two identical accounts
constexpr static auto dummyTransaction = R"({
"transaction":
{
"Account":"rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb",
"Amount":"40000000",
"Destination":"rDgGprMjMWkJRnJ8M5RXq3SXYD8zuQncPc",
"Fee":"20",
"Flags":2147483648,
"Sequence":13767283,
"SigningPubKey":"036F3CFFE1EA77C1EEC5DCCA38C83E62E3AC068F8A16369620AF1D609BA5A620B2",
"TransactionType":"Payment",
"TxnSignature":"30450221009BD0D563B24E50B26A42F30455AD21C3D5CD4D80174C41F7B54969FFC08DE94C02201FC35320B56D56D1E34D1D281D48AC68CBEDDD6EE9DFA639CCB08BB251453A87",
"hash":"F44393295DB860C6860769C16F5B23887762F09F87A8D1174E0FCFF9E7247F07"
}
})";
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<web::ConnectionBase> const 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 = dynamic_cast<MockSession*>(sessionIdle.get());
ASSERT_NE(rawIdle, nullptr);
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)
{
backend->setRange(10, 30);
boost::asio::io_context ctx;
auto ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30);
// mock fetchLedgerBySequence return this ledger
ON_CALL(*backend, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo));
EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1);
// mock doFetchLedgerObject return fee setting ledger object
auto feeBlob = CreateFeeSettingBlob(1, 2, 3, 4, 0);
ON_CALL(*backend, doFetchLedgerObject).WillByDefault(Return(feeBlob));
EXPECT_CALL(*backend, 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_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_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 const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
ripple::STObject const 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 const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
// create an empty meta object
ripple::STArray const 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",
"DeliverMax":"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,
"close_time_iso": "2000-01-01T00:00:00Z",
"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 const obj = CreateCreateOfferTransactionObject(ACCOUNT1, 1, 32, CURRENCY, ISSUER, 1, 3);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
ripple::STArray const 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));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(3);
ON_CALL(*backend, 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,
"close_time_iso": "2000-01-01T00:00:00Z",
"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,
"close_time_iso": "2000-01-01T00:00:00Z",
"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 const obj = CreateCreateOfferTransactionObject(ACCOUNT1, 1, 32, CURRENCY, ISSUER, 1, 3);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
ripple::STArray const 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));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(3);
ON_CALL(*backend, 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 const obj = CreateCreateOfferTransactionObject(ACCOUNT1, 1, 32, CURRENCY, ISSUER, 1, 3);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
ripple::STArray const 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));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(2);
auto kk = ripple::keylet::account(issueAccount).key;
ON_CALL(*backend, doFetchLedgerObject(testing::_, testing::_, testing::_))
.WillByDefault(Return(line.getSerializer().peekData()));
ripple::STObject const accountRoot = CreateAccountRootObject(ISSUER, ripple::lsfGlobalFreeze, 1, 10, 2, TXNID, 3);
ON_CALL(*backend, 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 const 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",
"DeliverMax":"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,
"close_time_iso": "2000-01-01T00:00:00Z",
"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 const 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",
"DeliverMax":"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",
"close_time_iso": "2000-01-01T00:00:00Z",
"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<web::ConnectionBase> const 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",
"DeliverMax":"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",
"close_time_iso": "2000-01-01T00:00:00Z",
"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",
"DeliverMax":"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",
"close_time_iso": "2000-01-01T00:00:00Z",
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
})";
std::shared_ptr<web::ConnectionBase> const 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

@@ -1,314 +0,0 @@
//------------------------------------------------------------------------------
/*
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 "feed/SubscriptionManager.h"
#include "util/Fixtures.h"
#include "util/MockPrometheus.h"
#include "util/MockWsBase.h"
#include "util/Taggable.h"
#include "util/config/Config.h"
#include "util/prometheus/Gauge.h"
#include "web/interface/ConnectionBase.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
#include <string>
using namespace feed;
using namespace util::prometheus;
// io_context
struct SubscriptionTestBase {
util::Config cfg;
util::TagDecoratorFactory tagDecoratorFactory{cfg};
};
struct SubscriptionTest : WithPrometheus, SyncAsioContextTest, SubscriptionTestBase {
Subscription sub{ctx, "test"};
};
// subscribe/unsubscribe the same session would not change the count
TEST_F(SubscriptionTest, SubscriptionCount)
{
std::shared_ptr<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<web::ConnectionBase> const 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_TRUE(sub.hasSession(session1));
EXPECT_TRUE(sub.hasSession(session2));
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());
EXPECT_FALSE(sub.hasSession(session1));
EXPECT_FALSE(sub.hasSession(session2));
}
// send interface will be called when publish called
TEST_F(SubscriptionTest, SubscriptionPublish)
{
std::shared_ptr<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(tagDecoratorFactory);
sub.subscribe(session1);
sub.subscribe(session2);
ctx.run();
EXPECT_EQ(sub.count(), 2);
sub.publish(std::make_shared<std::string>("message"));
ctx.restart();
ctx.run();
MockSession* p1 = dynamic_cast<MockSession*>(session1.get());
ASSERT_NE(p1, nullptr);
EXPECT_EQ(p1->message, "message");
MockSession* p2 = dynamic_cast<MockSession*>(session2.get());
ASSERT_NE(p2, nullptr);
EXPECT_EQ(p2->message, "message");
sub.unsubscribe(session1);
ctx.restart();
ctx.run();
sub.publish(std::make_shared<std::string>("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)
{
std::shared_ptr<web::ConnectionBase> const session1(new MockDeadSession(tagDecoratorFactory));
sub.subscribe(session1);
ctx.run();
EXPECT_EQ(sub.count(), 1);
// trigger dead
sub.publish(std::make_shared<std::string>("message"));
ctx.restart();
ctx.run();
EXPECT_EQ(session1->dead(), true);
sub.publish(std::make_shared<std::string>("message"));
ctx.restart();
ctx.run();
EXPECT_EQ(sub.count(), 0);
}
struct SubscriptionMockPrometheusTest : WithMockPrometheus, SubscriptionTestBase, SyncAsioContextTest {
Subscription sub{ctx, "test"};
std::shared_ptr<web::ConnectionBase> const session = std::make_shared<MockSession>(tagDecoratorFactory);
};
TEST_F(SubscriptionMockPrometheusTest, subscribe)
{
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"test\"}");
EXPECT_CALL(counter, add(1));
sub.subscribe(session);
ctx.run();
}
TEST_F(SubscriptionMockPrometheusTest, unsubscribe)
{
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"test\"}");
EXPECT_CALL(counter, add(1));
sub.subscribe(session);
ctx.run();
EXPECT_CALL(counter, add(-1));
sub.unsubscribe(session);
ctx.restart();
ctx.run();
}
TEST_F(SubscriptionMockPrometheusTest, publish)
{
auto deadSession = std::make_shared<MockDeadSession>(tagDecoratorFactory);
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"test\"}");
EXPECT_CALL(counter, add(1));
sub.subscribe(deadSession);
ctx.run();
EXPECT_CALL(counter, add(-1));
sub.publish(std::make_shared<std::string>("message"));
sub.publish(std::make_shared<std::string>("message")); // Dead session is detected only after failed send
ctx.restart();
ctx.run();
}
TEST_F(SubscriptionMockPrometheusTest, count)
{
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"test\"}");
EXPECT_CALL(counter, value());
sub.count();
}
struct SubscriptionMapTest : SubscriptionTest {
SubscriptionMap<std::string> subMap{ctx, "test"};
};
TEST_F(SubscriptionMapTest, SubscriptionMapCount)
{
std::shared_ptr<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<web::ConnectionBase> const session3 = std::make_shared<MockSession>(tagDecoratorFactory);
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);
EXPECT_TRUE(subMap.hasSession(session1, "topic1"));
EXPECT_TRUE(subMap.hasSession(session2, "topic1"));
EXPECT_TRUE(subMap.hasSession(session3, "topic2"));
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_FALSE(subMap.hasSession(session1, "topic1"));
EXPECT_FALSE(subMap.hasSession(session2, "topic1"));
EXPECT_FALSE(subMap.hasSession(session3, "topic2"));
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<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(tagDecoratorFactory);
std::string const topic1 = "topic1";
std::string const topic2 = "topic2";
std::string const topic1Message = "topic1Message";
std::string const topic2Message = "topic2Message";
subMap.subscribe(session1, topic1);
subMap.subscribe(session2, topic2);
ctx.run();
EXPECT_EQ(subMap.count(), 2);
auto message1 = std::make_shared<std::string>(topic1Message.data());
subMap.publish(message1, topic1); // lvalue
subMap.publish(std::make_shared<std::string>(topic2Message.data()), topic2); // rvalue
ctx.restart();
ctx.run();
MockSession* p1 = dynamic_cast<MockSession*>(session1.get());
ASSERT_NE(p1, nullptr);
EXPECT_EQ(p1->message, topic1Message);
MockSession* p2 = dynamic_cast<MockSession*>(session2.get());
ASSERT_NE(p2, nullptr);
EXPECT_EQ(p2->message, topic2Message);
}
TEST_F(SubscriptionMapTest, SubscriptionMapDeadRemoveSubscriber)
{
std::shared_ptr<web::ConnectionBase> const session1(new MockDeadSession(tagDecoratorFactory));
std::shared_ptr<web::ConnectionBase> const session2 = std::make_shared<MockSession>(tagDecoratorFactory);
std::string const topic1 = "topic1";
std::string const topic2 = "topic2";
std::string const topic1Message = "topic1Message";
std::string const topic2Message = "topic2Message";
subMap.subscribe(session1, topic1);
subMap.subscribe(session2, topic2);
ctx.run();
EXPECT_EQ(subMap.count(), 2);
auto message1 = std::make_shared<std::string>(topic1Message);
subMap.publish(message1, topic1); // lvalue
subMap.publish(std::make_shared<std::string>(topic2Message), topic2); // rvalue
ctx.restart();
ctx.run();
MockDeadSession* p1 = dynamic_cast<MockDeadSession*>(session1.get());
ASSERT_NE(p1, nullptr);
EXPECT_EQ(p1->dead(), true);
MockSession* p2 = dynamic_cast<MockSession*>(session2.get());
ASSERT_NE(p2, nullptr);
EXPECT_EQ(p2->message, topic2Message);
subMap.publish(message1, topic1);
ctx.restart();
ctx.run();
EXPECT_EQ(subMap.count(), 1);
}
struct SubscriptionMapMockPrometheusTest : SubscriptionMockPrometheusTest {
SubscriptionMap<std::string> subMap{ctx, "test"};
std::shared_ptr<web::ConnectionBase> const session = std::make_shared<MockSession>(tagDecoratorFactory);
};
TEST_F(SubscriptionMapMockPrometheusTest, subscribe)
{
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{collection=\"test\"}");
EXPECT_CALL(counter, add(1));
subMap.subscribe(session, "topic");
ctx.run();
}
TEST_F(SubscriptionMapMockPrometheusTest, unsubscribe)
{
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{collection=\"test\"}");
EXPECT_CALL(counter, add(1));
subMap.subscribe(session, "topic");
ctx.run();
EXPECT_CALL(counter, add(-1));
subMap.unsubscribe(session, "topic");
ctx.restart();
ctx.run();
}
TEST_F(SubscriptionMapMockPrometheusTest, publish)
{
auto deadSession = std::make_shared<MockDeadSession>(tagDecoratorFactory);
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{collection=\"test\"}");
EXPECT_CALL(counter, add(1));
subMap.subscribe(deadSession, "topic");
ctx.run();
EXPECT_CALL(counter, add(-1));
subMap.publish(std::make_shared<std::string>("message"), "topic");
subMap.publish(
std::make_shared<std::string>("message"), "topic"
); // Dead session is detected only after failed send
ctx.restart();
ctx.run();
}
TEST_F(SubscriptionMapMockPrometheusTest, count)
{
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{collection=\"test\"}");
EXPECT_CALL(counter, value());
subMap.count();
}

View File

@@ -0,0 +1,92 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, 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 "data/Types.h"
#include "feed/FeedBaseTest.h"
#include "feed/impl/BookChangesFeed.h"
#include "feed/impl/ForwardFeed.h"
#include "util/TestObject.h"
#include <boost/asio/io_context.hpp>
#include <boost/json/parse.hpp>
#include <gtest/gtest.h>
#include <ripple/protocol/STObject.h>
#include <memory>
#include <vector>
using namespace feed::impl;
namespace json = boost::json;
constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constexpr static auto ACCOUNT1 = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
constexpr static auto CURRENCY = "0158415500000000C1F76FF6ECB0BAC600000000";
constexpr static auto ISSUER = "rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD";
using FeedBookChangeTest = FeedBaseTest<BookChangesFeed>;
TEST_F(FeedBookChangeTest, Pub)
{
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 32);
auto transactions = std::vector<TransactionAndMetadata>{};
auto trans1 = TransactionAndMetadata();
ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
ripple::STObject const metaObj = CreateMetaDataForBookChange(CURRENCY, ISSUER, 22, 1, 3, 3, 1);
trans1.metadata = metaObj.getSerializer().peekData();
transactions.push_back(trans1);
testFeedPtr->pub(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"
}
]
})";
ctx.run();
EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(bookChangePublish));
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 0);
cleanReceivedFeed();
testFeedPtr->pub(ledgerinfo, transactions);
ctx.restart();
ctx.run();
EXPECT_TRUE(receivedFeedMessage().empty());
}

View File

@@ -0,0 +1,74 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, 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 "util/Fixtures.h"
#include "util/MockWsBase.h"
#include "util/Taggable.h"
#include "util/config/Config.h"
#include "web/interface/ConnectionBase.h"
#include <gtest/gtest.h>
#include <memory>
#include <string>
// Base class for feed tests, providing easy way to access the received feed
template <typename TestedFeed>
class FeedBaseTest : public SyncAsioContextTest, public MockBackendTest {
protected:
util::TagDecoratorFactory tagDecoratorFactory{util::Config{}};
std::shared_ptr<web::ConnectionBase> sessionPtr;
std::shared_ptr<TestedFeed> testFeedPtr;
void
SetUp() override
{
SyncAsioContextTest::SetUp();
MockBackendTest::SetUp();
testFeedPtr = std::make_shared<TestedFeed>(ctx);
sessionPtr = std::make_shared<MockSession>(tagDecoratorFactory);
}
void
TearDown() override
{
sessionPtr.reset();
testFeedPtr.reset();
MockBackendTest::TearDown();
SyncAsioContextTest::TearDown();
}
std::string const&
receivedFeedMessage() const
{
auto const mockSession = dynamic_cast<MockSession*>(sessionPtr.get());
[&] { ASSERT_NE(mockSession, nullptr); }();
return mockSession->message;
}
void
cleanReceivedFeed()
{
auto mockSession = dynamic_cast<MockSession*>(sessionPtr.get());
[&] { ASSERT_NE(mockSession, nullptr); }();
mockSession->message.clear();
}
};

View File

@@ -0,0 +1,73 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, 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 "feed/FeedBaseTest.h"
#include "feed/impl/ForwardFeed.h"
#include <boost/asio/io_context.hpp>
#include <boost/json/parse.hpp>
#include <gtest/gtest.h>
#include <memory>
using namespace feed::impl;
namespace json = boost::json;
using namespace util::prometheus;
constexpr static auto FEED = R"({"test":"test"})";
class NamedForwardFeedTest : public ForwardFeed {
public:
NamedForwardFeedTest(boost::asio::io_context& ioContext) : ForwardFeed(ioContext, "test")
{
}
};
using FeedForwardTest = FeedBaseTest<NamedForwardFeedTest>;
TEST_F(FeedForwardTest, Pub)
{
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
auto const json = json::parse(FEED).as_object();
testFeedPtr->pub(json);
ctx.run();
EXPECT_EQ(receivedFeedMessage(), FEED);
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 0);
cleanReceivedFeed();
testFeedPtr->pub(json);
ctx.restart();
ctx.run();
EXPECT_TRUE(receivedFeedMessage().empty());
}
TEST_F(FeedForwardTest, AutoDisconnect)
{
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
auto const json = json::parse(FEED).as_object();
testFeedPtr->pub(json);
ctx.run();
EXPECT_EQ(receivedFeedMessage(), FEED);
sessionPtr.reset();
EXPECT_EQ(testFeedPtr->count(), 0);
testFeedPtr->pub(json);
}

View File

@@ -0,0 +1,141 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, 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 "feed/FeedBaseTest.h"
#include "feed/impl/LedgerFeed.h"
#include "util/Fixtures.h"
#include "util/TestObject.h"
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <ripple/protocol/Fees.h>
#include <memory>
constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
using namespace feed::impl;
namespace json = boost::json;
using FeedLedgerTest = FeedBaseTest<LedgerFeed>;
TEST_F(FeedLedgerTest, SubPub)
{
backend->setRange(10, 30);
auto const ledgerInfo = CreateLedgerInfo(LEDGERHASH, 30);
EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(testing::Return(ledgerInfo));
auto const feeBlob = CreateFeeSettingBlob(1, 2, 3, 4, 0);
EXPECT_CALL(*backend, doFetchLedgerObject).WillOnce(testing::Return(feeBlob));
// 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_base":1,
"reserve_base":3,
"reserve_inc":2
})";
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto res = testFeedPtr->sub(yield, backend, sessionPtr);
// check the response
EXPECT_EQ(res, json::parse(LedgerResponse));
});
ctx.run();
EXPECT_EQ(testFeedPtr->count(), 1);
// test publish
auto const ledgerinfo2 = CreateLedgerInfo(LEDGERHASH, 31);
auto fee2 = ripple::Fees();
fee2.reserve = 10;
testFeedPtr->pub(ledgerinfo2, fee2, "10-31", 8);
constexpr static auto ledgerPub =
R"({
"type":"ledgerClosed",
"ledger_index":31,
"ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
"ledger_time":0,
"fee_base":0,
"reserve_base":10,
"reserve_inc":0,
"validated_ledgers":"10-31",
"txn_count":8
})";
ctx.restart();
ctx.run();
EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(ledgerPub));
// test unsub
cleanReceivedFeed();
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 0);
testFeedPtr->pub(ledgerinfo2, fee2, "10-31", 8);
ctx.restart();
ctx.run();
EXPECT_TRUE(receivedFeedMessage().empty());
}
TEST_F(FeedLedgerTest, AutoDisconnect)
{
backend->setRange(10, 30);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30);
EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(testing::Return(ledgerinfo));
auto const feeBlob = CreateFeeSettingBlob(1, 2, 3, 4, 0);
EXPECT_CALL(*backend, doFetchLedgerObject).WillOnce(testing::Return(feeBlob));
constexpr static auto LedgerResponse =
R"({
"validated_ledgers":"10-30",
"ledger_index":30,
"ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
"ledger_time":0,
"fee_base":1,
"reserve_base":3,
"reserve_inc":2
})";
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto res = testFeedPtr->sub(yield, backend, sessionPtr);
// check the response
EXPECT_EQ(res, json::parse(LedgerResponse));
});
ctx.run();
EXPECT_EQ(testFeedPtr->count(), 1);
// destroy the session
sessionPtr.reset();
EXPECT_EQ(testFeedPtr->count(), 0);
auto const ledgerinfo2 = CreateLedgerInfo(LEDGERHASH, 31);
auto fee2 = ripple::Fees();
fee2.reserve = 10;
// no error
testFeedPtr->pub(ledgerinfo2, fee2, "10-31", 8);
ctx.restart();
ctx.run();
}

View File

@@ -0,0 +1,325 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, 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 "feed/FeedBaseTest.h"
#include "feed/impl/ProposedTransactionFeed.h"
#include "util/Fixtures.h"
#include "util/MockPrometheus.h"
#include "util/MockWsBase.h"
#include "util/Taggable.h"
#include "util/TestObject.h"
#include "util/config/Config.h"
#include "util/prometheus/Gauge.h"
#include "web/interface/ConnectionBase.h"
#include <boost/asio/io_context.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
constexpr static auto ACCOUNT1 = "rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb";
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
constexpr static auto DUMMY_TRANSACTION =
R"({
"transaction":
{
"Account":"rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb",
"Amount":"40000000",
"Destination":"rDgGprMjMWkJRnJ8M5RXq3SXYD8zuQncPc",
"Fee":"20",
"Flags":2147483648,
"Sequence":13767283,
"SigningPubKey":"036F3CFFE1EA77C1EEC5DCCA38C83E62E3AC068F8A16369620AF1D609BA5A620B2",
"TransactionType":"Payment",
"TxnSignature":"30450221009BD0D563B24E50B26A42F30455AD21C3D5CD4D80174C41F7B54969FFC08DE94C02201FC35320B56D56D1E34D1D281D48AC68CBEDDD6EE9DFA639CCB08BB251453A87",
"hash":"F44393295DB860C6860769C16F5B23887762F09F87A8D1174E0FCFF9E7247F07"
}
})";
using namespace feed::impl;
namespace json = boost::json;
using namespace util::prometheus;
using FeedProposedTransactionTest = FeedBaseTest<ProposedTransactionFeed>;
TEST_F(FeedProposedTransactionTest, ProposedTransaction)
{
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.run();
EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(DUMMY_TRANSACTION));
cleanReceivedFeed();
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 0);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.restart();
ctx.run();
EXPECT_TRUE(receivedFeedMessage().empty());
}
TEST_F(FeedProposedTransactionTest, AccountProposedTransaction)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
testFeedPtr->sub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
std::shared_ptr<web::ConnectionBase> const sessionIdle = std::make_shared<MockSession>(tagDecoratorFactory);
auto const accountIdle = GetAccountIDWithString(ACCOUNT2);
testFeedPtr->sub(accountIdle, sessionIdle);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.run();
EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(DUMMY_TRANSACTION));
auto const rawIdle = dynamic_cast<MockSession*>(sessionIdle.get());
ASSERT_NE(rawIdle, nullptr);
EXPECT_TRUE(rawIdle->message.empty());
// unsub
cleanReceivedFeed();
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.restart();
ctx.run();
EXPECT_TRUE(receivedFeedMessage().empty());
}
TEST_F(FeedProposedTransactionTest, SubStreamAndAccount)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
testFeedPtr->sub(account, sessionPtr);
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.run();
EXPECT_EQ(receivedFeedMessage().size(), json::serialize(json::parse(DUMMY_TRANSACTION)).size() * 2);
cleanReceivedFeed();
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.restart();
ctx.run();
EXPECT_EQ(receivedFeedMessage().size(), json::serialize(json::parse(DUMMY_TRANSACTION)).size() * 2);
// unsub
cleanReceivedFeed();
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.restart();
ctx.run();
EXPECT_EQ(receivedFeedMessage().size(), json::serialize(json::parse(DUMMY_TRANSACTION)).size());
// unsub transaction
cleanReceivedFeed();
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 0);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.restart();
ctx.run();
EXPECT_TRUE(receivedFeedMessage().empty());
}
TEST_F(FeedProposedTransactionTest, AccountProposedTransactionDuplicate)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
auto const account2 = GetAccountIDWithString(ACCOUNT2);
testFeedPtr->sub(account, sessionPtr);
testFeedPtr->sub(account2, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
constexpr static auto dummyTransaction =
R"({
"transaction":
{
"Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"
}
})";
testFeedPtr->pub(json::parse(dummyTransaction).get_object());
ctx.run();
EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(dummyTransaction));
// unsub account1
cleanReceivedFeed();
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
testFeedPtr->pub(json::parse(dummyTransaction).get_object());
ctx.restart();
ctx.run();
EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(dummyTransaction));
// unsub account2
cleanReceivedFeed();
testFeedPtr->unsub(account2, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
testFeedPtr->pub(json::parse(dummyTransaction).get_object());
ctx.restart();
ctx.run();
EXPECT_TRUE(receivedFeedMessage().empty());
}
TEST_F(FeedProposedTransactionTest, Count)
{
testFeedPtr->sub(sessionPtr);
// repeat
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
auto const account1 = GetAccountIDWithString(ACCOUNT1);
testFeedPtr->sub(account1, sessionPtr);
// repeat
testFeedPtr->sub(account1, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
auto const sessionPtr2 = std::make_shared<MockSession>(tagDecoratorFactory);
testFeedPtr->sub(sessionPtr2);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 2);
auto const account2 = GetAccountIDWithString(ACCOUNT2);
testFeedPtr->sub(account2, sessionPtr2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
testFeedPtr->sub(account1, sessionPtr2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 3);
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
// unsub unsubscribed account
testFeedPtr->unsub(account2, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 3);
testFeedPtr->unsub(account1, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
testFeedPtr->unsub(account1, sessionPtr2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
testFeedPtr->unsub(account2, sessionPtr2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
}
TEST_F(FeedProposedTransactionTest, AutoDisconnect)
{
testFeedPtr->sub(sessionPtr);
// repeat
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
auto const account1 = GetAccountIDWithString(ACCOUNT1);
testFeedPtr->sub(account1, sessionPtr);
// repeat
testFeedPtr->sub(account1, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
auto sessionPtr2 = std::make_shared<MockSession>(tagDecoratorFactory);
testFeedPtr->sub(sessionPtr2);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 2);
auto const account2 = GetAccountIDWithString(ACCOUNT2);
testFeedPtr->sub(account2, sessionPtr2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
testFeedPtr->sub(account1, sessionPtr2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 3);
sessionPtr2.reset();
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
sessionPtr.reset();
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 0);
}
struct ProposedTransactionFeedMockPrometheusTest : WithMockPrometheus, SyncAsioContextTest {
protected:
util::TagDecoratorFactory tagDecoratorFactory{util::Config{}};
std::shared_ptr<web::ConnectionBase> sessionPtr;
std::shared_ptr<ProposedTransactionFeed> testFeedPtr;
void
SetUp() override
{
SyncAsioContextTest::SetUp();
testFeedPtr = std::make_shared<ProposedTransactionFeed>(ctx);
sessionPtr = std::make_shared<MockSession>(tagDecoratorFactory);
}
void
TearDown() override
{
sessionPtr.reset();
testFeedPtr.reset();
SyncAsioContextTest::TearDown();
}
};
TEST_F(ProposedTransactionFeedMockPrometheusTest, subUnsub)
{
auto& counterTx = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"tx_proposed\"}");
auto& counterAccount = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"account_proposed\"}");
EXPECT_CALL(counterTx, add(1));
EXPECT_CALL(counterTx, add(-1));
EXPECT_CALL(counterAccount, add(1));
EXPECT_CALL(counterAccount, add(-1));
testFeedPtr->sub(sessionPtr);
testFeedPtr->unsub(sessionPtr);
auto const account = GetAccountIDWithString(ACCOUNT1);
testFeedPtr->sub(account, sessionPtr);
testFeedPtr->unsub(account, sessionPtr);
}
TEST_F(ProposedTransactionFeedMockPrometheusTest, AutoDisconnect)
{
auto& counterTx = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"tx_proposed\"}");
auto& counterAccount = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"account_proposed\"}");
EXPECT_CALL(counterTx, add(1));
EXPECT_CALL(counterTx, add(-1));
EXPECT_CALL(counterAccount, add(1));
EXPECT_CALL(counterAccount, add(-1));
testFeedPtr->sub(sessionPtr);
auto const account = GetAccountIDWithString(ACCOUNT1);
testFeedPtr->sub(account, sessionPtr);
sessionPtr.reset();
}

View File

@@ -0,0 +1,131 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, 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 "feed/FeedBaseTest.h"
#include "feed/impl/SingleFeedBase.h"
#include "util/Fixtures.h"
#include "util/MockPrometheus.h"
#include "util/MockWsBase.h"
#include "util/Taggable.h"
#include "util/config/Config.h"
#include "util/prometheus/Gauge.h"
#include "web/interface/ConnectionBase.h"
#include <boost/asio/io_context.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
constexpr static auto FEED = R"({"test":"test"})";
using namespace feed::impl;
using namespace util::prometheus;
struct FeedBaseMockPrometheusTest : WithMockPrometheus, SyncAsioContextTest {
protected:
util::TagDecoratorFactory tagDecoratorFactory{util::Config{}};
std::shared_ptr<web::ConnectionBase> sessionPtr;
std::shared_ptr<SingleFeedBase> testFeedPtr;
void
SetUp() override
{
SyncAsioContextTest::SetUp();
testFeedPtr = std::make_shared<SingleFeedBase>(ctx, "testFeed");
sessionPtr = std::make_shared<MockSession>(tagDecoratorFactory);
}
void
TearDown() override
{
sessionPtr.reset();
testFeedPtr.reset();
SyncAsioContextTest::TearDown();
}
};
TEST_F(FeedBaseMockPrometheusTest, subUnsub)
{
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"testFeed\"}");
EXPECT_CALL(counter, add(1));
EXPECT_CALL(counter, add(-1));
testFeedPtr->sub(sessionPtr);
testFeedPtr->unsub(sessionPtr);
}
TEST_F(FeedBaseMockPrometheusTest, AutoUnsub)
{
auto& counter = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"testFeed\"}");
EXPECT_CALL(counter, add(1));
EXPECT_CALL(counter, add(-1));
testFeedPtr->sub(sessionPtr);
sessionPtr.reset();
}
class NamedSingleFeedTest : public SingleFeedBase {
public:
NamedSingleFeedTest(boost::asio::io_context& ioContext) : SingleFeedBase(ioContext, "forTest")
{
}
};
using SingleFeedBaseTest = FeedBaseTest<NamedSingleFeedTest>;
TEST_F(SingleFeedBaseTest, Test)
{
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
testFeedPtr->pub(FEED);
ctx.run();
EXPECT_EQ(receivedFeedMessage(), FEED);
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 0);
cleanReceivedFeed();
testFeedPtr->pub(FEED);
ctx.restart();
ctx.run();
EXPECT_TRUE(receivedFeedMessage().empty());
}
TEST_F(SingleFeedBaseTest, TestAutoDisconnect)
{
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
testFeedPtr->pub(FEED);
ctx.run();
EXPECT_EQ(receivedFeedMessage(), FEED);
sessionPtr.reset();
EXPECT_EQ(testFeedPtr->count(), 0);
}
TEST_F(SingleFeedBaseTest, RepeatSub)
{
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 0);
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 0);
}

View File

@@ -0,0 +1,484 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, 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 "data/Types.h"
#include "feed/SubscriptionManager.h"
#include "util/Fixtures.h"
#include "util/MockWsBase.h"
#include "util/Taggable.h"
#include "util/TestObject.h"
#include "util/config/Config.h"
#include "web/interface/ConnectionBase.h"
#include <boost/asio/executor_work_guard.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <ripple/protocol/Book.h>
#include <ripple/protocol/Fees.h>
#include <ripple/protocol/Issue.h>
#include <ripple/protocol/STObject.h>
#include <chrono>
#include <memory>
#include <optional>
#include <string>
#include <thread>
#include <vector>
constexpr static auto ACCOUNT1 = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
constexpr static auto CURRENCY = "0158415500000000C1F76FF6ECB0BAC600000000";
constexpr static auto ISSUER = "rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD";
constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
namespace json = boost::json;
using namespace feed;
using namespace feed::impl;
class SubscriptionManagerTest : public MockBackendTest, public SyncAsioContextTest {
protected:
util::Config cfg;
std::shared_ptr<SubscriptionManager> SubscriptionManagerPtr;
util::TagDecoratorFactory tagDecoratorFactory{cfg};
std::shared_ptr<web::ConnectionBase> session;
void
SetUp() override
{
MockBackendTest::SetUp();
SyncAsioContextTest::SetUp();
SubscriptionManagerPtr = std::make_shared<SubscriptionManager>(ctx, backend);
session = std::make_shared<MockSession>(tagDecoratorFactory);
}
void
TearDown() override
{
session.reset();
SubscriptionManagerPtr.reset();
SyncAsioContextTest::TearDown();
MockBackendTest::TearDown();
}
std::string const&
receivedFeedMessage() const
{
auto const mockSession = dynamic_cast<MockSession*>(session.get());
[&] { ASSERT_NE(mockSession, nullptr); }();
return mockSession->message;
}
void
cleanReceivedFeed()
{
auto mockSession = dynamic_cast<MockSession*>(session.get());
[&] { ASSERT_NE(mockSession, nullptr); }();
mockSession->message.clear();
}
};
TEST_F(SubscriptionManagerTest, MultipleThreadCtx)
{
std::optional<boost::asio::io_context::work> work_;
work_.emplace(ctx); // guard the context
std::vector<std::thread> workers;
workers.reserve(2);
for (int i = 0; i < 2; ++i)
workers.emplace_back([this]() { ctx.run(); });
SubscriptionManagerPtr->subManifest(session);
SubscriptionManagerPtr->subValidation(session);
SubscriptionManagerPtr->forwardManifest(json::parse(R"({"manifest":"test"})").get_object());
SubscriptionManagerPtr->forwardValidation(json::parse(R"({"validation":"test"})").get_object());
auto retry = 5;
while (--retry != 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
if (receivedFeedMessage() == R"({"manifest":"test"}{"validation":"test"})" ||
receivedFeedMessage() == R"({"validation":"test"}{"manifest":"test"})")
break;
}
EXPECT_TRUE(retry != 0) << "receivedFeedMessage() = " << receivedFeedMessage();
session.reset();
work_.reset();
for (auto& worker : workers)
worker.join();
SubscriptionManagerPtr.reset();
}
TEST_F(SubscriptionManagerTest, MultipleThreadCtxSessionDieEarly)
{
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> work_ = boost::asio::make_work_guard(ctx);
std::vector<std::thread> workers;
workers.reserve(2);
for (int i = 0; i < 2; ++i)
workers.emplace_back([this]() { ctx.run(); });
SubscriptionManagerPtr->subManifest(session);
SubscriptionManagerPtr->subValidation(session);
SubscriptionManagerPtr->forwardManifest(json::parse(R"({"manifest":"test"})").get_object());
SubscriptionManagerPtr->forwardValidation(json::parse(R"({"validation":"test"})").get_object());
session.reset();
work_.reset();
for (auto& worker : workers)
worker.join();
// SubscriptionManager's pub job is running in thread pool, so we let thread pool run out of work, otherwise
// SubscriptionManager will die before the job is called
SubscriptionManagerPtr.reset();
}
TEST_F(SubscriptionManagerTest, 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<web::ConnectionBase> const session1 = std::make_shared<MockSession>(tagDecoratorFactory);
std::shared_ptr<web::ConnectionBase> session2 = std::make_shared<MockSession>(tagDecoratorFactory);
SubscriptionManagerPtr->subBookChanges(session1);
SubscriptionManagerPtr->subBookChanges(session2);
SubscriptionManagerPtr->subManifest(session1);
SubscriptionManagerPtr->subManifest(session2);
SubscriptionManagerPtr->subProposedTransactions(session1);
SubscriptionManagerPtr->subProposedTransactions(session2);
SubscriptionManagerPtr->subTransactions(session1, 1);
SubscriptionManagerPtr->subTransactions(session2, 2);
SubscriptionManagerPtr->subValidation(session1);
SubscriptionManagerPtr->subValidation(session2);
auto const account = GetAccountIDWithString(ACCOUNT1);
SubscriptionManagerPtr->subAccount(account, session1, 1);
SubscriptionManagerPtr->subAccount(account, session2, 2);
SubscriptionManagerPtr->subProposedAccount(account, session1);
SubscriptionManagerPtr->subProposedAccount(account, session2);
auto const issue1 = GetIssue(CURRENCY, ISSUER);
ripple::Book const book{ripple::xrpIssue(), issue1};
SubscriptionManagerPtr->subBook(book, session1, 1);
SubscriptionManagerPtr->subBook(book, session2, 2);
EXPECT_EQ(SubscriptionManagerPtr->report(), json::parse(ReportReturn));
// count down when unsub manually
SubscriptionManagerPtr->unsubBookChanges(session1);
SubscriptionManagerPtr->unsubManifest(session1);
SubscriptionManagerPtr->unsubProposedTransactions(session1);
SubscriptionManagerPtr->unsubTransactions(session1);
SubscriptionManagerPtr->unsubValidation(session1);
SubscriptionManagerPtr->unsubAccount(account, session1);
SubscriptionManagerPtr->unsubProposedAccount(account, session1);
SubscriptionManagerPtr->unsubBook(book, session1);
// try to unsub an account which is not subscribed
auto const account2 = GetAccountIDWithString(ACCOUNT2);
SubscriptionManagerPtr->unsubAccount(account2, session1);
SubscriptionManagerPtr->unsubProposedAccount(account2, session1);
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(SubscriptionManagerPtr->report(), 1);
// count down when session disconnect
session2.reset();
checkResult(SubscriptionManagerPtr->report(), 0);
}
TEST_F(SubscriptionManagerTest, ManifestTest)
{
SubscriptionManagerPtr->subManifest(session);
constexpr static auto dummyManifest = R"({"manifest":"test"})";
SubscriptionManagerPtr->forwardManifest(json::parse(dummyManifest).get_object());
ctx.run();
EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(dummyManifest));
cleanReceivedFeed();
SubscriptionManagerPtr->unsubManifest(session);
SubscriptionManagerPtr->forwardManifest(json::parse(dummyManifest).get_object());
ctx.run();
EXPECT_TRUE(receivedFeedMessage().empty());
}
TEST_F(SubscriptionManagerTest, ValidationTest)
{
SubscriptionManagerPtr->subValidation(session);
constexpr static auto dummyManifest = R"({"validation":"test"})";
SubscriptionManagerPtr->forwardValidation(json::parse(dummyManifest).get_object());
ctx.run();
EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(dummyManifest));
cleanReceivedFeed();
SubscriptionManagerPtr->unsubValidation(session);
SubscriptionManagerPtr->forwardValidation(json::parse(dummyManifest).get_object());
ctx.restart();
ctx.run();
EXPECT_TRUE(receivedFeedMessage().empty());
}
TEST_F(SubscriptionManagerTest, BookChangesTest)
{
SubscriptionManagerPtr->subBookChanges(session);
EXPECT_EQ(SubscriptionManagerPtr->report()["book_changes"], 1);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 32);
auto transactions = std::vector<TransactionAndMetadata>{};
auto trans1 = TransactionAndMetadata();
ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
ripple::STObject const metaObj = CreateMetaDataForBookChange(CURRENCY, ISSUER, 22, 1, 3, 3, 1);
trans1.metadata = metaObj.getSerializer().peekData();
transactions.push_back(trans1);
SubscriptionManagerPtr->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"
}
]
})";
ctx.run();
EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(bookChangePublish));
SubscriptionManagerPtr->unsubBookChanges(session);
EXPECT_EQ(SubscriptionManagerPtr->report()["book_changes"], 0);
}
TEST_F(SubscriptionManagerTest, LedgerTest)
{
backend->setRange(10, 30);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30);
EXPECT_CALL(*backend, fetchLedgerBySequence).WillOnce(testing::Return(ledgerinfo));
auto const feeBlob = CreateFeeSettingBlob(1, 2, 3, 4, 0);
EXPECT_CALL(*backend, doFetchLedgerObject).WillOnce(testing::Return(feeBlob));
// 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_base":1,
"reserve_base":3,
"reserve_inc":2
})";
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto const res = SubscriptionManagerPtr->subLedger(yield, session);
// check the response
EXPECT_EQ(res, json::parse(LedgerResponse));
});
ctx.run();
EXPECT_EQ(SubscriptionManagerPtr->report()["ledger"], 1);
// test publish
auto const ledgerinfo2 = CreateLedgerInfo(LEDGERHASH, 31);
auto fee2 = ripple::Fees();
fee2.reserve = 10;
SubscriptionManagerPtr->pubLedger(ledgerinfo2, fee2, "10-31", 8);
constexpr static auto ledgerPub =
R"({
"type":"ledgerClosed",
"ledger_index":31,
"ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
"ledger_time":0,
"fee_base":0,
"reserve_base":10,
"reserve_inc":0,
"validated_ledgers":"10-31",
"txn_count":8
})";
ctx.restart();
ctx.run();
EXPECT_EQ(json::parse(receivedFeedMessage()), json::parse(ledgerPub));
// test unsub
SubscriptionManagerPtr->unsubLedger(session);
EXPECT_EQ(SubscriptionManagerPtr->report()["ledger"], 0);
}
TEST_F(SubscriptionManagerTest, TransactionTest)
{
auto const issue1 = GetIssue(CURRENCY, ISSUER);
auto const account = GetAccountIDWithString(ISSUER);
ripple::Book const book{ripple::xrpIssue(), issue1};
SubscriptionManagerPtr->subBook(book, session, 1);
SubscriptionManagerPtr->subTransactions(session, 1);
SubscriptionManagerPtr->subAccount(account, session, 1);
EXPECT_EQ(SubscriptionManagerPtr->report()["account"], 1);
EXPECT_EQ(SubscriptionManagerPtr->report()["transactions"], 1);
EXPECT_EQ(SubscriptionManagerPtr->report()["books"], 1);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33);
auto trans1 = TransactionAndMetadata();
auto obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
auto const metaObj = CreateMetaDataForBookChange(CURRENCY, ISSUER, 22, 3, 1, 1, 3);
trans1.metadata = metaObj.getSerializer().peekData();
SubscriptionManagerPtr->pubTransaction(trans1, ledgerinfo);
constexpr static auto OrderbookPublish =
R"({
"transaction":
{
"Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Amount":"1",
"DeliverMax":"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",
"close_time_iso": "2000-01-01T00:00:00Z",
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
})";
ctx.run();
EXPECT_EQ(receivedFeedMessage().size(), json::serialize(json::parse(OrderbookPublish)).size() * 3);
SubscriptionManagerPtr->unsubBook(book, session);
SubscriptionManagerPtr->unsubTransactions(session);
SubscriptionManagerPtr->unsubAccount(account, session);
EXPECT_EQ(SubscriptionManagerPtr->report()["account"], 0);
EXPECT_EQ(SubscriptionManagerPtr->report()["transactions"], 0);
EXPECT_EQ(SubscriptionManagerPtr->report()["books"], 0);
}
TEST_F(SubscriptionManagerTest, ProposedTransactionTest)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
SubscriptionManagerPtr->subProposedAccount(account, session);
SubscriptionManagerPtr->subProposedTransactions(session);
EXPECT_EQ(SubscriptionManagerPtr->report()["accounts_proposed"], 1);
EXPECT_EQ(SubscriptionManagerPtr->report()["transactions_proposed"], 1);
constexpr static auto dummyTransaction =
R"({
"transaction":
{
"Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun"
}
})";
SubscriptionManagerPtr->forwardProposedTransaction(json::parse(dummyTransaction).get_object());
ctx.run();
EXPECT_EQ(receivedFeedMessage().size(), json::serialize(json::parse(dummyTransaction)).size() * 2);
// unsub account1
cleanReceivedFeed();
SubscriptionManagerPtr->unsubProposedAccount(account, session);
EXPECT_EQ(SubscriptionManagerPtr->report()["accounts_proposed"], 0);
SubscriptionManagerPtr->unsubProposedTransactions(session);
EXPECT_EQ(SubscriptionManagerPtr->report()["transactions_proposed"], 0);
}

View File

@@ -0,0 +1,142 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, 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 "feed/impl/TrackableSignal.h"
#include "feed/impl/TrackableSignalMap.h"
#include "util/MockWsBase.h"
#include "util/Taggable.h"
#include "util/config/Config.h"
#include "web/interface/ConnectionBase.h"
#include <gtest/gtest.h>
#include <memory>
#include <string>
using namespace testing;
struct FeedTrackableSignalTests : Test {
protected:
util::TagDecoratorFactory tagDecoratorFactory{util::Config{}};
std::shared_ptr<web::ConnectionBase> sessionPtr;
void
SetUp() override
{
sessionPtr = std::make_shared<MockSession>(tagDecoratorFactory);
}
void
TearDown() override
{
sessionPtr.reset();
}
};
TEST_F(FeedTrackableSignalTests, Connect)
{
feed::impl::TrackableSignal<web::ConnectionBase, std::string> signal;
std::string testString;
auto const slot = [&](std::string const& s) { testString += s; };
EXPECT_TRUE(signal.connectTrackableSlot(sessionPtr, slot));
EXPECT_FALSE(signal.connectTrackableSlot(sessionPtr, slot));
EXPECT_EQ(signal.count(), 1);
signal.emit("test");
EXPECT_EQ(testString, "test");
EXPECT_TRUE(signal.disconnect(sessionPtr.get()));
EXPECT_EQ(signal.count(), 0);
EXPECT_FALSE(signal.disconnect(sessionPtr.get()));
testString.clear();
signal.emit("test2");
EXPECT_TRUE(testString.empty());
}
TEST_F(FeedTrackableSignalTests, AutoDisconnect)
{
feed::impl::TrackableSignal<web::ConnectionBase, std::string> signal;
std::string testString;
auto const slot = [&](std::string const& s) { testString += s; };
EXPECT_TRUE(signal.connectTrackableSlot(sessionPtr, slot));
EXPECT_FALSE(signal.connectTrackableSlot(sessionPtr, slot));
EXPECT_EQ(signal.count(), 1);
signal.emit("test");
EXPECT_EQ(testString, "test");
sessionPtr.reset();
// track object is destroyed, but the connection is still there
EXPECT_EQ(signal.count(), 1);
testString.clear();
signal.emit("test2");
EXPECT_TRUE(testString.empty());
}
TEST_F(FeedTrackableSignalTests, MapConnect)
{
feed::impl::TrackableSignalMap<std::string, web::ConnectionBase, std::string> signalMap;
std::string testString;
auto const slot = [&](std::string const& s) { testString += s; };
EXPECT_TRUE(signalMap.connectTrackableSlot(sessionPtr, "test", slot));
EXPECT_TRUE(signalMap.connectTrackableSlot(sessionPtr, "test1", slot));
EXPECT_FALSE(signalMap.connectTrackableSlot(sessionPtr, "test", slot));
signalMap.emit("test", "test");
signalMap.emit("test2", "test2");
EXPECT_EQ(testString, "test");
EXPECT_TRUE(signalMap.disconnect(sessionPtr.get(), "test"));
EXPECT_FALSE(signalMap.disconnect(sessionPtr.get(), "test"));
testString.clear();
signalMap.emit("test", "test2");
EXPECT_TRUE(testString.empty());
signalMap.emit("test1", "test1");
EXPECT_EQ(testString, "test1");
}
TEST_F(FeedTrackableSignalTests, MapAutoDisconnect)
{
feed::impl::TrackableSignalMap<std::string, web::ConnectionBase, std::string> signalMap;
std::string testString;
auto const slot = [&](std::string const& s) { testString += s; };
EXPECT_TRUE(signalMap.connectTrackableSlot(sessionPtr, "test", slot));
EXPECT_TRUE(signalMap.connectTrackableSlot(sessionPtr, "test1", slot));
EXPECT_FALSE(signalMap.connectTrackableSlot(sessionPtr, "test", slot));
signalMap.emit("test", "test");
signalMap.emit("test2", "test2");
EXPECT_EQ(testString, "test");
// kill trackable
sessionPtr.reset();
testString.clear();
signalMap.emit("test", "test");
EXPECT_TRUE(testString.empty());
signalMap.emit("test1", "test1");
EXPECT_TRUE(testString.empty());
}

File diff suppressed because it is too large Load Diff

View File

@@ -70,9 +70,9 @@ protected:
SetUp() override
{
HandlerBaseTest::SetUp();
util::Config const cfg;
subManager_ = feed::SubscriptionManager::make_SubscriptionManager(cfg, backend);
util::TagDecoratorFactory const tagDecoratorFactory{cfg};
subManager_ = std::make_shared<feed::SubscriptionManager>(ctx, backend);
util::TagDecoratorFactory const tagDecoratorFactory{util::Config{}};
session_ = std::make_shared<MockSession>(tagDecoratorFactory);
}
void

View File

@@ -16,7 +16,6 @@
*/
//==============================================================================
#include "feed/SubscriptionManager.h"
#include "rpc/Errors.h"
#include "util/Fixtures.h"
#include "util/MockETLService.h"
@@ -66,7 +65,7 @@ struct MockWsBase : public web::ConnectionBase {
}
};
class WebRPCServerHandlerTest : public MockBackendTest {
class WebRPCServerHandlerTest : public MockBackendTest, public SyncAsioContextTest {
protected:
void
SetUp() override
@@ -76,11 +75,8 @@ protected:
etl = std::make_shared<MockETLService>();
rpcEngine = std::make_shared<MockAsyncRPCEngine>();
tagFactory = std::make_shared<util::TagDecoratorFactory>(cfg);
subManager = std::make_shared<SubscriptionManager>(cfg, backend);
session = std::make_shared<MockWsBase>(*tagFactory);
handler = std::make_shared<RPCServerHandler<MockAsyncRPCEngine, MockETLService>>(
cfg, backend, rpcEngine, etl, subManager
);
handler = std::make_shared<RPCServerHandler<MockAsyncRPCEngine, MockETLService>>(cfg, backend, rpcEngine, etl);
}
void
@@ -91,7 +87,6 @@ protected:
std::shared_ptr<MockAsyncRPCEngine> rpcEngine;
std::shared_ptr<MockETLService> etl;
std::shared_ptr<SubscriptionManager> subManager;
std::shared_ptr<util::TagDecoratorFactory> tagFactory;
std::shared_ptr<RPCServerHandler<MockAsyncRPCEngine, MockETLService>> handler;
std::shared_ptr<MockWsBase> session;
@@ -703,9 +698,8 @@ TEST_F(WebRPCServerHandlerTest, WsTooBusy)
session->upgraded = true;
auto localRpcEngine = std::make_shared<MockRPCEngine>();
auto localHandler = std::make_shared<RPCServerHandler<MockRPCEngine, MockETLService>>(
cfg, backend, localRpcEngine, etl, subManager
);
auto localHandler =
std::make_shared<RPCServerHandler<MockRPCEngine, MockETLService>>(cfg, backend, localRpcEngine, etl);
static auto constexpr request = R"({
"command": "server_info",
"id": 99
@@ -732,9 +726,8 @@ TEST_F(WebRPCServerHandlerTest, WsTooBusy)
TEST_F(WebRPCServerHandlerTest, HTTPTooBusy)
{
auto localRpcEngine = std::make_shared<MockRPCEngine>();
auto localHandler = std::make_shared<RPCServerHandler<MockRPCEngine, MockETLService>>(
cfg, backend, localRpcEngine, etl, subManager
);
auto localHandler =
std::make_shared<RPCServerHandler<MockRPCEngine, MockETLService>>(cfg, backend, localRpcEngine, etl);
static auto constexpr request = R"({
"method": "server_info",
"params": [{}]