Separate unit tests and integration tests (#1393)

Fixes #1391
This commit is contained in:
Alex Kremer
2024-05-07 15:12:48 +01:00
committed by GitHub
parent 98ef83d470
commit cbc856b190
177 changed files with 168 additions and 106 deletions

View File

@@ -0,0 +1,88 @@
//------------------------------------------------------------------------------
/*
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.hpp"
#include "feed/FeedTestUtil.hpp"
#include "feed/impl/BookChangesFeed.hpp"
#include "feed/impl/ForwardFeed.hpp"
#include "util/TestObject.hpp"
#include <boost/asio/io_context.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <ripple/protocol/STObject.h>
#include <vector>
using namespace feed::impl;
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"
}
]
})";
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(bookChangePublish))).Times(1);
ctx.run();
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 0);
testFeedPtr->pub(ledgerinfo, transactions);
ctx.restart();
ctx.run();
}

View File

@@ -0,0 +1,72 @@
//------------------------------------------------------------------------------
/*
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/FeedTestUtil.hpp"
#include "feed/impl/ForwardFeed.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
#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);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED))).Times(1);
ctx.run();
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 0);
testFeedPtr->pub(json);
ctx.restart();
ctx.run();
}
TEST_F(FeedForwardTest, AutoDisconnect)
{
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
auto const json = json::parse(FEED).as_object();
testFeedPtr->pub(json);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED))).Times(1);
ctx.run();
sessionPtr.reset();
EXPECT_EQ(testFeedPtr->count(), 0);
testFeedPtr->pub(json);
}

View File

@@ -0,0 +1,137 @@
//------------------------------------------------------------------------------
/*
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/FeedTestUtil.hpp"
#include "feed/impl/LedgerFeed.hpp"
#include "util/TestObject.hpp"
#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>
constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
using namespace feed::impl;
namespace json = boost::json;
using namespace testing;
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 = CreateLegacyFeeSettingBlob(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);
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
})";
// test publish
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(ledgerPub))).Times(1);
auto const ledgerinfo2 = CreateLedgerInfo(LEDGERHASH, 31);
auto fee2 = ripple::Fees();
fee2.reserve = 10;
testFeedPtr->pub(ledgerinfo2, fee2, "10-31", 8);
ctx.restart();
ctx.run();
// test unsub, after unsub the send should not be called
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 0);
EXPECT_CALL(*mockSessionPtr, send(_)).Times(0);
testFeedPtr->pub(ledgerinfo2, fee2, "10-31", 8);
ctx.restart();
ctx.run();
}
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 = CreateLegacyFeeSettingBlob(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);
EXPECT_CALL(*mockSessionPtr, send(_)).Times(0);
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,289 @@
//------------------------------------------------------------------------------
/*
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/FeedTestUtil.hpp"
#include "feed/impl/ProposedTransactionFeed.hpp"
#include "util/Fixtures.hpp"
#include "util/MockPrometheus.hpp"
#include "util/MockWsBase.hpp"
#include "util/TestObject.hpp"
#include "util/prometheus/Gauge.hpp"
#include "web/interface/ConnectionBase.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
constexpr static auto ACCOUNT1 = "rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb";
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
constexpr static auto ACCOUNT3 = "r92yNeoiCdwULRbjh6cUBEbD71iHcqe1hE";
constexpr static auto DUMMY_TRANSACTION =
R"({
"transaction":
{
"Account":"rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb",
"Amount":"40000000",
"Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun",
"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);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.run();
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 0);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.restart();
ctx.run();
}
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>();
auto const accountIdle = GetAccountIDWithString(ACCOUNT3);
testFeedPtr->sub(accountIdle, sessionIdle);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.run();
// unsub
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.restart();
ctx.run();
}
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);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(2);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.run();
// unsub
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.restart();
ctx.run();
// unsub transaction
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 0);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.restart();
ctx.run();
}
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);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.run();
// unsub account1
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.restart();
ctx.run();
// unsub account2
testFeedPtr->unsub(account2, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
ctx.restart();
ctx.run();
}
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>();
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>();
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:
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>();
}
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,128 @@
//------------------------------------------------------------------------------
/*
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/FeedTestUtil.hpp"
#include "feed/impl/SingleFeedBase.hpp"
#include "util/Fixtures.hpp"
#include "util/MockPrometheus.hpp"
#include "util/MockWsBase.hpp"
#include "util/prometheus/Gauge.hpp"
#include "web/interface/ConnectionBase.hpp"
#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:
std::shared_ptr<web::ConnectionBase> sessionPtr;
std::shared_ptr<SingleFeedBase> testFeedPtr;
MockSession* mockSessionPtr = nullptr;
void
SetUp() override
{
SyncAsioContextTest::SetUp();
testFeedPtr = std::make_shared<SingleFeedBase>(ctx, "testFeed");
sessionPtr = std::make_shared<MockSession>();
mockSessionPtr = dynamic_cast<MockSession*>(sessionPtr.get());
}
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)
{
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED))).Times(1);
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
testFeedPtr->pub(FEED);
ctx.run();
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 0);
testFeedPtr->pub(FEED);
ctx.restart();
ctx.run();
}
TEST_F(SingleFeedBaseTest, TestAutoDisconnect)
{
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED))).Times(1);
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
testFeedPtr->pub(FEED);
ctx.run();
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,454 @@
//------------------------------------------------------------------------------
/*
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.hpp"
#include "feed/FeedTestUtil.hpp"
#include "feed/SubscriptionManager.hpp"
#include "util/Fixtures.hpp"
#include "util/MockPrometheus.hpp"
#include "util/MockWsBase.hpp"
#include "util/TestObject.hpp"
#include "web/interface/ConnectionBase.hpp"
#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 <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 <memory>
#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 util::prometheus::WithPrometheus,
public MockBackendTest,
public SyncAsioContextTest {
protected:
std::shared_ptr<SubscriptionManager> SubscriptionManagerPtr;
std::shared_ptr<web::ConnectionBase> session;
MockSession* sessionPtr = nullptr;
void
SetUp() override
{
MockBackendTest::SetUp();
SyncAsioContextTest::SetUp();
SubscriptionManagerPtr = std::make_shared<SubscriptionManager>(ctx, backend);
session = std::make_shared<MockSession>();
sessionPtr = dynamic_cast<MockSession*>(session.get());
}
void
TearDown() override
{
session.reset();
SubscriptionManagerPtr.reset();
SyncAsioContextTest::TearDown();
MockBackendTest::TearDown();
}
};
// TODO enable when fixed :/
/*
TEST_F(SubscriptionManagerTest, MultipleThreadCtx)
{
std::vector<std::thread> workers;
workers.reserve(2);
SubscriptionManagerPtr->subManifest(session);
SubscriptionManagerPtr->subValidation(session);
constexpr static auto jsonManifest = R"({"manifest":"test"})";
constexpr static auto jsonValidation = R"({"validation":"test"})";
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(jsonManifest))).Times(1);
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(jsonValidation))).Times(1);
SubscriptionManagerPtr->forwardManifest(json::parse(jsonManifest).get_object());
SubscriptionManagerPtr->forwardValidation(json::parse(jsonValidation).get_object());
for (int i = 0; i < 2; ++i)
workers.emplace_back([this]() { ctx.run(); });
// wait for all jobs in ctx to finish
for (auto& worker : workers)
worker.join();
session.reset();
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>();
std::shared_ptr<web::ConnectionBase> session2 = std::make_shared<MockSession>();
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)
{
constexpr static auto dummyManifest = R"({"manifest":"test"})";
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(dummyManifest))).Times(1);
SubscriptionManagerPtr->subManifest(session);
SubscriptionManagerPtr->forwardManifest(json::parse(dummyManifest).get_object());
ctx.run();
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(dummyManifest))).Times(0);
SubscriptionManagerPtr->unsubManifest(session);
SubscriptionManagerPtr->forwardManifest(json::parse(dummyManifest).get_object());
ctx.run();
}
TEST_F(SubscriptionManagerTest, ValidationTest)
{
constexpr static auto dummy = R"({"validation":"test"})";
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(dummy))).Times(1);
SubscriptionManagerPtr->subValidation(session);
SubscriptionManagerPtr->forwardValidation(json::parse(dummy).get_object());
ctx.run();
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(dummy))).Times(0);
SubscriptionManagerPtr->unsubValidation(session);
SubscriptionManagerPtr->forwardValidation(json::parse(dummy).get_object());
ctx.restart();
ctx.run();
}
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"
}
]
})";
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(bookChangePublish))).Times(1);
ctx.run();
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 = CreateLegacyFeeSettingBlob(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
})";
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(ledgerPub))).Times(1);
ctx.restart();
ctx.run();
// 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":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
"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."
})";
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(OrderbookPublish))).Times(3);
ctx.run();
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"
}
})";
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(dummyTransaction))).Times(2);
SubscriptionManagerPtr->forwardProposedTransaction(json::parse(dummyTransaction).get_object());
ctx.run();
// unsub account1
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,138 @@
//------------------------------------------------------------------------------
/*
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.hpp"
#include "feed/impl/TrackableSignalMap.hpp"
#include "util/MockWsBase.hpp"
#include "web/interface/ConnectionBase.hpp"
#include <gtest/gtest.h>
#include <memory>
#include <string>
using namespace testing;
struct FeedTrackableSignalTests : Test {
protected:
std::shared_ptr<web::ConnectionBase> sessionPtr;
void
SetUp() override
{
sessionPtr = std::make_shared<MockSession>();
}
void
TearDown() override
{
}
};
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());
}

View File

@@ -0,0 +1,994 @@
//------------------------------------------------------------------------------
/*
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.hpp"
#include "feed/FeedTestUtil.hpp"
#include "feed/impl/TransactionFeed.hpp"
#include "util/Fixtures.hpp"
#include "util/MockPrometheus.hpp"
#include "util/MockWsBase.hpp"
#include "util/TestObject.hpp"
#include "util/prometheus/Gauge.hpp"
#include "web/interface/ConnectionBase.hpp"
#include <boost/asio/io_context.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <ripple/basics/base_uint.h>
#include <ripple/protocol/Book.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/STObject.h>
#include <ripple/protocol/TER.h>
#include <memory>
constexpr static auto ACCOUNT1 = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
constexpr static auto LEDGERHASH = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
constexpr static auto CURRENCY = "0158415500000000C1F76FF6ECB0BAC600000000";
constexpr static auto ISSUER = "rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD";
constexpr static auto TXNID = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
constexpr static auto TRAN_V1 =
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",
"Balance":"110"
},
"LedgerEntryType":"AccountRoot"
}
},
{
"ModifiedNode":
{
"FinalFields":
{
"Account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun",
"Balance":"30"
},
"LedgerEntryType":"AccountRoot"
}
}
],
"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."
})";
constexpr static auto TRAN_V2 =
R"({
"tx_json":
{
"Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"DeliverMax":"1",
"Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun",
"Fee":"1",
"Sequence":32,
"SigningPubKey":"74657374",
"TransactionType":"Payment",
"date":0
},
"meta":
{
"AffectedNodes":
[
{
"ModifiedNode":{
"FinalFields":{
"Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Balance":"110"
},
"LedgerEntryType":"AccountRoot"
}
},
{
"ModifiedNode":{
"FinalFields":{
"Account":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun",
"Balance":"30"
},
"LedgerEntryType":"AccountRoot"
}
}
],
"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",
"hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2",
"engine_result_code":0,
"engine_result":"tesSUCCESS",
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
})";
using namespace feed::impl;
using namespace util::prometheus;
using FeedTransactionTest = FeedBaseTest<TransactionFeed>;
TEST_F(FeedTransactionTest, SubTransactionV1)
{
testFeedPtr->sub(sessionPtr, 1);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 1);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33);
auto trans1 = TransactionAndMetadata();
ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
testFeedPtr->pub(trans1, ledgerinfo, backend);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
ctx.run();
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 0);
testFeedPtr->pub(trans1, ledgerinfo, backend);
ctx.restart();
ctx.run();
}
TEST_F(FeedTransactionTest, SubTransactionV2)
{
testFeedPtr->sub(sessionPtr, 2);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 1);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33);
auto trans1 = TransactionAndMetadata();
ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
testFeedPtr->pub(trans1, ledgerinfo, backend);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
ctx.run();
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 0);
testFeedPtr->pub(trans1, ledgerinfo, backend);
ctx.restart();
ctx.run();
}
TEST_F(FeedTransactionTest, SubAccountV1)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
testFeedPtr->sub(account, sessionPtr, 1);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33);
auto trans1 = TransactionAndMetadata();
ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
testFeedPtr->pub(trans1, ledgerinfo, backend);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
ctx.run();
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
testFeedPtr->pub(trans1, ledgerinfo, backend);
ctx.restart();
ctx.run();
}
TEST_F(FeedTransactionTest, SubAccountV2)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
testFeedPtr->sub(account, sessionPtr, 2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33);
auto trans1 = TransactionAndMetadata();
ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
testFeedPtr->pub(trans1, ledgerinfo, backend);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
ctx.run();
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
testFeedPtr->pub(trans1, ledgerinfo, backend);
ctx.restart();
ctx.run();
}
TEST_F(FeedTransactionTest, SubBothTransactionAndAccount)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
testFeedPtr->sub(account, sessionPtr, 2);
testFeedPtr->sub(sessionPtr, 2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 1);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33);
auto trans1 = TransactionAndMetadata();
ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
testFeedPtr->pub(trans1, ledgerinfo, backend);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(2);
ctx.run();
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 0);
testFeedPtr->pub(trans1, ledgerinfo, backend);
ctx.restart();
ctx.run();
}
TEST_F(FeedTransactionTest, SubBookV1)
{
auto const issue1 = GetIssue(CURRENCY, ISSUER);
ripple::Book const book{ripple::xrpIssue(), issue1};
testFeedPtr->sub(book, sessionPtr, 1);
EXPECT_EQ(testFeedPtr->bookSubCount(), 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 metaObj = CreateMetaDataForBookChange(CURRENCY, ISSUER, 22, 3, 1, 1, 3);
trans1.metadata = metaObj.getSerializer().peekData();
testFeedPtr->pub(trans1, ledgerinfo, backend);
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."
})";
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(OrderbookPublish))).Times(1);
ctx.run();
// trigger by offer cancel meta data
metaObj = CreateMetaDataForCancelOffer(CURRENCY, ISSUER, 22, 3, 1);
trans1.metadata = metaObj.getSerializer().peekData();
testFeedPtr->pub(trans1, ledgerinfo, backend);
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."
})";
ctx.restart();
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(OrderbookCancelPublish))).Times(1);
ctx.run();
// 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."
})";
metaObj = CreateMetaDataForCreateOffer(CURRENCY, ISSUER, 22, 3, 1);
trans1.metadata = metaObj.getSerializer().peekData();
testFeedPtr->pub(trans1, ledgerinfo, backend);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(OrderbookCreatePublish))).Times(1);
ctx.restart();
ctx.run();
testFeedPtr->unsub(book, sessionPtr);
EXPECT_EQ(testFeedPtr->bookSubCount(), 0);
testFeedPtr->pub(trans1, ledgerinfo, backend);
ctx.restart();
ctx.run();
}
TEST_F(FeedTransactionTest, SubBookV2)
{
auto const issue1 = GetIssue(CURRENCY, ISSUER);
ripple::Book const book{ripple::xrpIssue(), issue1};
testFeedPtr->sub(book, sessionPtr, 2);
EXPECT_EQ(testFeedPtr->bookSubCount(), 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();
testFeedPtr->pub(trans1, ledgerinfo, backend);
constexpr static auto OrderbookPublish =
R"({
"tx_json":
{
"Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"DeliverMax":"1",
"Destination":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun",
"Fee":"1",
"Sequence":32,
"SigningPubKey":"74657374",
"TransactionType":"Payment",
"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",
"hash":"51D2AAA6B8E4E16EF22F6424854283D8391B56875858A711B8CE4D5B9A422CC2",
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
})";
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(OrderbookPublish))).Times(1);
ctx.run();
testFeedPtr->unsub(book, sessionPtr);
EXPECT_EQ(testFeedPtr->bookSubCount(), 0);
testFeedPtr->pub(trans1, ledgerinfo, backend);
ctx.restart();
ctx.run();
}
TEST_F(FeedTransactionTest, TransactionContainsBothAccountsSubed)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
testFeedPtr->sub(account, sessionPtr, 2);
auto const account2 = GetAccountIDWithString(ACCOUNT2);
testFeedPtr->sub(account2, sessionPtr, 2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33);
auto trans1 = TransactionAndMetadata();
ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
testFeedPtr->pub(trans1, ledgerinfo, backend);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
ctx.run();
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
testFeedPtr->pub(trans1, ledgerinfo, backend);
ctx.restart();
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
ctx.run();
testFeedPtr->unsub(account2, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
testFeedPtr->pub(trans1, ledgerinfo, backend);
ctx.restart();
ctx.run();
}
TEST_F(FeedTransactionTest, SubAccountRepeatWithDifferentVersion)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
testFeedPtr->sub(account, sessionPtr, 1);
auto const account2 = GetAccountIDWithString(ACCOUNT2);
testFeedPtr->sub(account2, sessionPtr, 2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33);
auto trans1 = TransactionAndMetadata();
ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
testFeedPtr->pub(trans1, ledgerinfo, backend);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
ctx.run();
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
testFeedPtr->pub(trans1, ledgerinfo, backend);
ctx.restart();
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
ctx.run();
testFeedPtr->unsub(account2, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
testFeedPtr->pub(trans1, ledgerinfo, backend);
ctx.restart();
ctx.run();
}
TEST_F(FeedTransactionTest, SubTransactionRepeatWithDifferentVersion)
{
// sub version 1 first
testFeedPtr->sub(sessionPtr, 1);
// sub version 2 later
testFeedPtr->sub(sessionPtr, 2);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 1);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 33);
auto trans1 = TransactionAndMetadata();
ripple::STObject const obj = CreatePaymentTransactionObject(ACCOUNT1, ACCOUNT2, 1, 1, 32);
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
testFeedPtr->pub(trans1, ledgerinfo, backend);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
ctx.run();
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 0);
testFeedPtr->pub(trans1, ledgerinfo, backend);
ctx.restart();
ctx.run();
}
TEST_F(FeedTransactionTest, SubRepeat)
{
auto const session2 = std::make_shared<MockSession>();
testFeedPtr->sub(sessionPtr, 1);
testFeedPtr->sub(session2, 1);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 2);
testFeedPtr->sub(sessionPtr, 1);
testFeedPtr->sub(session2, 1);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 2);
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 1);
testFeedPtr->unsub(session2);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 0);
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 0);
auto const account = GetAccountIDWithString(ACCOUNT1);
auto const account2 = GetAccountIDWithString(ACCOUNT2);
testFeedPtr->sub(account, sessionPtr, 1);
testFeedPtr->sub(account2, session2, 1);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
testFeedPtr->sub(account, sessionPtr, 1);
testFeedPtr->sub(account2, session2, 1);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
testFeedPtr->unsub(account2, session2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
auto const issue1 = GetIssue(CURRENCY, ISSUER);
ripple::Book const book{ripple::xrpIssue(), issue1};
testFeedPtr->sub(book, sessionPtr, 1);
EXPECT_EQ(testFeedPtr->bookSubCount(), 1);
testFeedPtr->sub(book, session2, 1);
EXPECT_EQ(testFeedPtr->bookSubCount(), 2);
testFeedPtr->unsub(book, sessionPtr);
EXPECT_EQ(testFeedPtr->bookSubCount(), 1);
testFeedPtr->unsub(book, session2);
EXPECT_EQ(testFeedPtr->bookSubCount(), 0);
testFeedPtr->unsub(book, sessionPtr);
EXPECT_EQ(testFeedPtr->bookSubCount(), 0);
}
TEST_F(FeedTransactionTest, PubTransactionWithOwnerFund)
{
testFeedPtr->sub(sessionPtr, 1);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 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 const issue2 = GetIssue(CURRENCY, ISSUER);
line.setFieldAmount(ripple::sfBalance, ripple::STAmount(issue2, 100));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(3);
auto const issueAccount = GetAccountIDWithString(ISSUER);
auto const kk = ripple::keylet::account(issueAccount).key;
ON_CALL(*backend, doFetchLedgerObject(testing::_, testing::_, testing::_))
.WillByDefault(testing::Return(line.getSerializer().peekData()));
ripple::STObject const accountRoot = CreateAccountRootObject(ISSUER, 0, 1, 10, 2, TXNID, 3);
ON_CALL(*backend, doFetchLedgerObject(kk, testing::_, testing::_))
.WillByDefault(testing::Return(accountRoot.getSerializer().peekData()));
testFeedPtr->pub(trans1, ledgerinfo, backend);
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."
})";
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TransactionForOwnerFund))).Times(1);
ctx.run();
}
constexpr static auto TRAN_FROZEN =
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_F(FeedTransactionTest, PubTransactionOfferCreationFrozenLine)
{
testFeedPtr->sub(sessionPtr, 1);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 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);
auto const issueAccount = GetAccountIDWithString(ISSUER);
auto const kk = ripple::keylet::account(issueAccount).key;
ON_CALL(*backend, doFetchLedgerObject(testing::_, testing::_, testing::_))
.WillByDefault(testing::Return(line.getSerializer().peekData()));
ripple::STObject const accountRoot = CreateAccountRootObject(ISSUER, 0, 1, 10, 2, TXNID, 3);
ON_CALL(*backend, doFetchLedgerObject(kk, testing::_, testing::_))
.WillByDefault(testing::Return(accountRoot.getSerializer().peekData()));
testFeedPtr->pub(trans1, ledgerinfo, backend);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_FROZEN))).Times(1);
ctx.run();
}
TEST_F(FeedTransactionTest, SubTransactionOfferCreationGlobalFrozen)
{
testFeedPtr->sub(sessionPtr, 1);
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 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 const issueAccount = GetAccountIDWithString(ISSUER);
line.setFieldAmount(ripple::sfBalance, ripple::STAmount(GetIssue(CURRENCY, ISSUER), 100));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(2);
auto const kk = ripple::keylet::account(issueAccount).key;
ON_CALL(*backend, doFetchLedgerObject(testing::_, testing::_, testing::_))
.WillByDefault(testing::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(testing::Return(accountRoot.getSerializer().peekData()));
testFeedPtr->pub(trans1, ledgerinfo, backend);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_FROZEN))).Times(1);
ctx.run();
}
struct TransactionFeedMockPrometheusTest : WithMockPrometheus, SyncAsioContextTest {
protected:
std::shared_ptr<web::ConnectionBase> sessionPtr;
std::shared_ptr<TransactionFeed> testFeedPtr;
void
SetUp() override
{
SyncAsioContextTest::SetUp();
testFeedPtr = std::make_shared<TransactionFeed>(ctx);
sessionPtr = std::make_shared<MockSession>();
}
void
TearDown() override
{
sessionPtr.reset();
testFeedPtr.reset();
SyncAsioContextTest::TearDown();
}
};
TEST_F(TransactionFeedMockPrometheusTest, subUnsub)
{
auto& counterTx = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"tx\"}");
auto& counterAccount = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"account\"}");
auto& counterBook = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"book\"}");
EXPECT_CALL(counterTx, add(1));
EXPECT_CALL(counterTx, add(-1));
EXPECT_CALL(counterAccount, add(1));
EXPECT_CALL(counterAccount, add(-1));
EXPECT_CALL(counterBook, add(1));
EXPECT_CALL(counterBook, add(-1));
testFeedPtr->sub(sessionPtr, 1);
testFeedPtr->unsub(sessionPtr);
auto const account = GetAccountIDWithString(ACCOUNT1);
testFeedPtr->sub(account, sessionPtr, 1);
testFeedPtr->unsub(account, sessionPtr);
auto const issue1 = GetIssue(CURRENCY, ISSUER);
ripple::Book const book{ripple::xrpIssue(), issue1};
testFeedPtr->sub(book, sessionPtr, 1);
testFeedPtr->unsub(book, sessionPtr);
}
TEST_F(TransactionFeedMockPrometheusTest, AutoDisconnect)
{
auto& counterTx = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"tx\"}");
auto& counterAccount = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"account\"}");
auto& counterBook = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"book\"}");
EXPECT_CALL(counterTx, add(1));
EXPECT_CALL(counterTx, add(-1));
EXPECT_CALL(counterAccount, add(1));
EXPECT_CALL(counterAccount, add(-1));
EXPECT_CALL(counterBook, add(1));
EXPECT_CALL(counterBook, add(-1));
testFeedPtr->sub(sessionPtr, 1);
auto const account = GetAccountIDWithString(ACCOUNT1);
testFeedPtr->sub(account, sessionPtr, 1);
auto const issue1 = GetIssue(CURRENCY, ISSUER);
ripple::Book const book{ripple::xrpIssue(), issue1};
testFeedPtr->sub(book, sessionPtr, 1);
sessionPtr.reset();
}