feat: Integrate new webserver (#1722)

For #919.
The new web server is not using dosguard yet. It will be fixed by a
separate PR.
This commit is contained in:
Sergey Kuznetsov
2024-11-21 14:48:32 +00:00
committed by GitHub
parent fc3ba07f2e
commit c77154a5e6
90 changed files with 4029 additions and 683 deletions

View File

@@ -23,7 +23,7 @@
#include "util/MockPrometheus.hpp"
#include "util/MockWsBase.hpp"
#include "util/SyncExecutionCtxFixture.hpp"
#include "web/interface/ConnectionBase.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include <boost/json/parse.hpp>
#include <gtest/gtest.h>
@@ -37,25 +37,9 @@
template <typename TestedFeed>
struct FeedBaseTest : util::prometheus::WithPrometheus, MockBackendTest, SyncExecutionCtxFixture {
protected:
std::shared_ptr<web::ConnectionBase> sessionPtr;
std::shared_ptr<TestedFeed> testFeedPtr;
MockSession* mockSessionPtr = nullptr;
void
SetUp() override
{
testFeedPtr = std::make_shared<TestedFeed>(ctx);
sessionPtr = std::make_shared<MockSession>();
sessionPtr->apiSubVersion = 1;
mockSessionPtr = dynamic_cast<MockSession*>(sessionPtr.get());
}
void
TearDown() override
{
sessionPtr.reset();
testFeedPtr.reset();
}
web::SubscriptionContextPtr sessionPtr = std::make_shared<MockSession>();
std::shared_ptr<TestedFeed> testFeedPtr = std::make_shared<TestedFeed>(ctx);
MockSession* mockSessionPtr = dynamic_cast<MockSession*>(sessionPtr.get());
};
namespace impl {

View File

@@ -34,22 +34,7 @@ template <template <typename> typename MockType = ::testing::NiceMock>
struct HandlerBaseTestBase : util::prometheus::WithPrometheus,
MockBackendTestBase<MockType>,
SyncAsioContextTest,
MockETLServiceTestBase<MockType> {
protected:
void
SetUp() override
{
SyncAsioContextTest::SetUp();
MockETLServiceTestBase<MockType>::SetUp();
}
void
TearDown() override
{
MockETLServiceTestBase<MockType>::TearDown();
SyncAsioContextTest::TearDown();
}
};
MockETLServiceTestBase<MockType> {};
/**
* @brief Fixture with a "nice" backend mock and an embedded boost::asio context.

View File

@@ -21,20 +21,25 @@
#include "util/Taggable.hpp"
#include "util/config/Config.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/interface/ConnectionBase.hpp"
#include <boost/beast/http/status.hpp>
#include <gmock/gmock.h>
#include <cstdint>
#include <memory>
#include <string>
struct MockSession : public web::ConnectionBase {
struct MockSession : public web::SubscriptionContextInterface {
MOCK_METHOD(void, send, (std::shared_ptr<std::string>), (override));
MOCK_METHOD(void, send, (std::string&&, boost::beast::http::status), (override));
MOCK_METHOD(void, onDisconnect, (OnDisconnectSlot const&), (override));
MOCK_METHOD(void, setApiSubversion, (uint32_t), (override));
MOCK_METHOD(uint32_t, apiSubversion, (), (const, override));
util::TagDecoratorFactory tagDecoratorFactory{util::Config{}};
MockSession() : web::ConnectionBase(tagDecoratorFactory, "")
MockSession() : web::SubscriptionContextInterface(tagDecoratorFactory)
{
}
};

View File

@@ -29,6 +29,7 @@
#include <boost/asio/ssl/stream_base.hpp>
#include <boost/asio/ssl/verify_context.hpp>
#include <boost/asio/ssl/verify_mode.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <boost/beast/core/error.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/core/stream_traits.hpp>

View File

@@ -0,0 +1,45 @@
//------------------------------------------------------------------------------
/*
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/Taggable.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/interface/ConnectionBase.hpp"
#include <boost/beast/http/status.hpp>
#include <gmock/gmock.h>
#include <memory>
#include <string>
struct ConnectionBaseMock : web::ConnectionBase {
using ConnectionBase::ConnectionBase;
MOCK_METHOD(void, send, (std::string&&, boost::beast::http::status), (override));
MOCK_METHOD(void, send, (std::shared_ptr<std::string>), (override));
MOCK_METHOD(
web::SubscriptionContextPtr,
makeSubscriptionContext,
(util::TagDecoratorFactory const& factory),
(override)
);
};
using ConnectionBaseStrictMockPtr = std::shared_ptr<testing::StrictMock<ConnectionBaseMock>>;

View File

@@ -19,18 +19,29 @@
#pragma once
#include "util/Taggable.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/HttpConnection.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <gmock/gmock.h>
#include <chrono>
#include <memory>
#include <optional>
struct MockConnectionMetadataImpl : web::ng::ConnectionMetadata {
using web::ng::ConnectionMetadata::ConnectionMetadata;
MOCK_METHOD(bool, wasUpgraded, (), (const, override));
};
using MockConnectionMetadata = testing::NiceMock<MockConnectionMetadataImpl>;
using StrictMockConnectionMetadata = testing::StrictMock<MockConnectionMetadataImpl>;
struct MockConnectionImpl : web::ng::Connection {
using web::ng::Connection::Connection;

View File

@@ -0,0 +1,85 @@
//------------------------------------------------------------------------------
/*
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/Taggable.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/HttpConnection.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <gmock/gmock.h>
#include <chrono>
#include <memory>
#include <optional>
struct MockHttpConnectionImpl : web::ng::impl::UpgradableConnection {
using UpgradableConnection::UpgradableConnection;
MOCK_METHOD(bool, wasUpgraded, (), (const, override));
using SendReturnType = std::optional<web::ng::Error>;
MOCK_METHOD(
SendReturnType,
send,
(web::ng::Response, boost::asio::yield_context, std::chrono::steady_clock::duration),
(override)
);
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
MOCK_METHOD(
ReceiveReturnType,
receive,
(boost::asio::yield_context, std::chrono::steady_clock::duration),
(override)
);
MOCK_METHOD(void, close, (boost::asio::yield_context, std::chrono::steady_clock::duration));
using IsUpgradeRequestedReturnType = std::expected<bool, web::ng::Error>;
MOCK_METHOD(
IsUpgradeRequestedReturnType,
isUpgradeRequested,
(boost::asio::yield_context, std::chrono::steady_clock::duration),
(override)
);
using UpgradeReturnType = std::expected<web::ng::ConnectionPtr, web::ng::Error>;
using OptionalSslContext = std::optional<boost::asio::ssl::context>;
MOCK_METHOD(
UpgradeReturnType,
upgrade,
(OptionalSslContext & sslContext,
util::TagDecoratorFactory const& tagDecoratorFactory,
boost::asio::yield_context yield),
(override)
);
};
using MockHttpConnection = testing::NiceMock<MockHttpConnectionImpl>;
using MockHttpConnectionPtr = std::unique_ptr<testing::NiceMock<MockHttpConnectionImpl>>;
using StrictMockHttpConnection = testing::StrictMock<MockHttpConnectionImpl>;
using StrictMockHttpConnectionPtr = std::unique_ptr<testing::StrictMock<MockHttpConnectionImpl>>;

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.
*/
//==============================================================================
#pragma once
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/WsConnection.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <gmock/gmock.h>
#include <chrono>
#include <memory>
#include <optional>
struct MockWsConnectionImpl : web::ng::impl::WsConnectionBase {
using WsConnectionBase::WsConnectionBase;
MOCK_METHOD(bool, wasUpgraded, (), (const, override));
using SendReturnType = std::optional<web::ng::Error>;
MOCK_METHOD(
SendReturnType,
send,
(web::ng::Response, boost::asio::yield_context, std::chrono::steady_clock::duration),
(override)
);
using ReceiveReturnType = std::expected<web::ng::Request, web::ng::Error>;
MOCK_METHOD(
ReceiveReturnType,
receive,
(boost::asio::yield_context, std::chrono::steady_clock::duration),
(override)
);
MOCK_METHOD(void, close, (boost::asio::yield_context, std::chrono::steady_clock::duration));
using SendBufferReturnType = std::optional<web::ng::Error>;
MOCK_METHOD(
SendBufferReturnType,
sendBuffer,
(boost::asio::const_buffer, boost::asio::yield_context, std::chrono::steady_clock::duration),
(override)
);
};
using MockWsConnection = testing::NiceMock<MockWsConnectionImpl>;
using MockWsConnectionPtr = std::unique_ptr<testing::NiceMock<MockWsConnectionImpl>>;
using StrictMockWsConnection = testing::StrictMock<MockWsConnectionImpl>;
using StrictMockWsConnectionPtr = std::unique_ptr<testing::StrictMock<MockWsConnectionImpl>>;

View File

@@ -134,19 +134,24 @@ target_sources(
util/TxUtilTests.cpp
util/WithTimeout.cpp
# Webserver
web/AdminVerificationTests.cpp
web/dosguard/DOSGuardTests.cpp
web/dosguard/IntervalSweepHandlerTests.cpp
web/dosguard/WhitelistHandlerTests.cpp
web/impl/AdminVerificationTests.cpp
web/impl/ErrorHandlingTests.cpp
web/ng/ResponseTests.cpp
web/ng/RequestTests.cpp
web/ng/RPCServerHandlerTests.cpp
web/ng/ServerTests.cpp
web/ng/SubscriptionContextTests.cpp
web/ng/impl/ConnectionHandlerTests.cpp
web/ng/impl/ErrorHandlingTests.cpp
web/ng/impl/HttpConnectionTests.cpp
web/ng/impl/ServerSslContextTests.cpp
web/ng/impl/WsConnectionTests.cpp
web/RPCServerHandlerTests.cpp
web/ServerTests.cpp
web/SubscriptionContextTests.cpp
# New Config
util/newconfig/ArrayTests.cpp
util/newconfig/ArrayViewTests.cpp

View File

@@ -41,11 +41,27 @@ TEST_F(CliArgsTests, Parse_NoArgs)
int const returnCode = 123;
EXPECT_CALL(onRunMock, Call).WillOnce([](CliArgs::Action::Run const& run) {
EXPECT_EQ(run.configPath, CliArgs::defaultConfigPath);
EXPECT_FALSE(run.useNgWebServer);
return returnCode;
});
EXPECT_EQ(action.apply(onRunMock.AsStdFunction(), onExitMock.AsStdFunction()), returnCode);
}
TEST_F(CliArgsTests, Parse_NgWebServer)
{
for (auto& argv : {std::array{"clio_server", "-w"}, std::array{"clio_server", "--ng-web-server"}}) {
auto const action = CliArgs::parse(argv.size(), const_cast<char const**>(argv.data()));
int const returnCode = 123;
EXPECT_CALL(onRunMock, Call).WillOnce([](CliArgs::Action::Run const& run) {
EXPECT_EQ(run.configPath, CliArgs::defaultConfigPath);
EXPECT_TRUE(run.useNgWebServer);
return returnCode;
});
EXPECT_EQ(action.apply(onRunMock.AsStdFunction(), onExitMock.AsStdFunction()), returnCode);
}
}
TEST_F(CliArgsTests, Parse_VersionHelp)
{
for (auto& argv :

View File

@@ -41,6 +41,7 @@ using FeedBookChangeTest = FeedBaseTest<BookChangesFeed>;
TEST_F(FeedBookChangeTest, Pub)
{
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);

View File

@@ -20,6 +20,7 @@
#include "feed/FeedTestUtil.hpp"
#include "feed/impl/ForwardFeed.hpp"
#include "util/async/AnyExecutionContext.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
@@ -44,11 +45,14 @@ using FeedForwardTest = FeedBaseTest<NamedForwardFeedTest>;
TEST_F(FeedForwardTest, Pub)
{
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
auto const json = json::parse(FEED).as_object();
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED))).Times(1);
testFeedPtr->pub(json);
testFeedPtr->unsub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 0);
testFeedPtr->pub(json);
@@ -56,12 +60,18 @@ TEST_F(FeedForwardTest, Pub)
TEST_F(FeedForwardTest, AutoDisconnect)
{
web::SubscriptionContextInterface::OnDisconnectSlot slot;
EXPECT_CALL(*mockSessionPtr, onDisconnect).WillOnce(testing::SaveArg<0>(&slot));
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
auto const json = json::parse(FEED).as_object();
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED))).Times(1);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED)));
testFeedPtr->pub(json);
slot(sessionPtr.get());
sessionPtr.reset();
EXPECT_EQ(testFeedPtr->count(), 0);
testFeedPtr->pub(json);
}

View File

@@ -20,6 +20,7 @@
#include "feed/FeedTestUtil.hpp"
#include "feed/impl/LedgerFeed.hpp"
#include "util/TestObject.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
@@ -60,6 +61,7 @@ TEST_F(FeedLedgerTest, SubPub)
})";
boost::asio::io_context ioContext;
boost::asio::spawn(ioContext, [this](boost::asio::yield_context yield) {
EXPECT_CALL(*mockSessionPtr, onDisconnect);
auto res = testFeedPtr->sub(yield, backend, sessionPtr);
// check the response
EXPECT_EQ(res, json::parse(LedgerResponse));
@@ -112,6 +114,10 @@ TEST_F(FeedLedgerTest, AutoDisconnect)
"reserve_base":3,
"reserve_inc":2
})";
web::SubscriptionContextInterface::OnDisconnectSlot slot;
EXPECT_CALL(*mockSessionPtr, onDisconnect).WillOnce(testing::SaveArg<0>(&slot));
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto res = testFeedPtr->sub(yield, backend, sessionPtr);
// check the response
@@ -120,7 +126,10 @@ TEST_F(FeedLedgerTest, AutoDisconnect)
EXPECT_EQ(testFeedPtr->count(), 1);
EXPECT_CALL(*mockSessionPtr, send(_)).Times(0);
ASSERT_TRUE(slot);
slot(sessionPtr.get());
sessionPtr.reset();
EXPECT_EQ(testFeedPtr->count(), 0);
auto const ledgerHeader2 = CreateLedgerHeader(LEDGERHASH, 31);

View File

@@ -24,13 +24,15 @@
#include "util/SyncExecutionCtxFixture.hpp"
#include "util/TestObject.hpp"
#include "util/prometheus/Gauge.hpp"
#include "web/interface/ConnectionBase.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <memory>
#include <vector>
constexpr static auto ACCOUNT1 = "rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb";
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
@@ -60,10 +62,11 @@ using FeedProposedTransactionTest = FeedBaseTest<ProposedTransactionFeed>;
TEST_F(FeedProposedTransactionTest, ProposedTransaction)
{
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION)));
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
testFeedPtr->unsub(sessionPtr);
@@ -75,15 +78,19 @@ TEST_F(FeedProposedTransactionTest, ProposedTransaction)
TEST_F(FeedProposedTransactionTest, AccountProposedTransaction)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
std::shared_ptr<web::ConnectionBase> const sessionIdle = std::make_shared<MockSession>();
web::SubscriptionContextPtr const sessionIdle = std::make_shared<MockSession>();
auto const accountIdle = GetAccountIDWithString(ACCOUNT3);
EXPECT_CALL(*dynamic_cast<MockSession*>(sessionIdle.get()), onDisconnect);
testFeedPtr->sub(accountIdle, sessionIdle);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION)));
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
@@ -97,8 +104,11 @@ TEST_F(FeedProposedTransactionTest, AccountProposedTransaction)
TEST_F(FeedProposedTransactionTest, SubStreamAndAccount)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*mockSessionPtr, onDisconnect).Times(2);
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);
@@ -108,7 +118,7 @@ TEST_F(FeedProposedTransactionTest, SubStreamAndAccount)
// unsub
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION)));
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
@@ -124,17 +134,18 @@ TEST_F(FeedProposedTransactionTest, AccountProposedTransactionDuplicate)
auto const account = GetAccountIDWithString(ACCOUNT1);
auto const account2 = GetAccountIDWithString(ACCOUNT2);
EXPECT_CALL(*mockSessionPtr, onDisconnect).Times(2);
testFeedPtr->sub(account, sessionPtr);
testFeedPtr->sub(account2, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION)));
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
// unsub account1
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION))).Times(1);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(DUMMY_TRANSACTION)));
testFeedPtr->pub(json::parse(DUMMY_TRANSACTION).get_object());
// unsub account2
@@ -146,24 +157,33 @@ TEST_F(FeedProposedTransactionTest, AccountProposedTransactionDuplicate)
TEST_F(FeedProposedTransactionTest, Count)
{
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
// repeat
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
auto const account1 = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(account1, sessionPtr);
// repeat
testFeedPtr->sub(account1, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
auto const sessionPtr2 = std::make_shared<MockSession>();
EXPECT_CALL(*dynamic_cast<MockSession*>(sessionPtr2.get()), onDisconnect);
testFeedPtr->sub(sessionPtr2);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 2);
auto const account2 = GetAccountIDWithString(ACCOUNT2);
EXPECT_CALL(*dynamic_cast<MockSession*>(sessionPtr2.get()), onDisconnect);
testFeedPtr->sub(account2, sessionPtr2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
EXPECT_CALL(*dynamic_cast<MockSession*>(sessionPtr2.get()), onDisconnect);
testFeedPtr->sub(account1, sessionPtr2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 3);
@@ -184,31 +204,51 @@ TEST_F(FeedProposedTransactionTest, Count)
TEST_F(FeedProposedTransactionTest, AutoDisconnect)
{
std::vector<web::SubscriptionContextInterface::OnDisconnectSlot> sessionOnDisconnectSlots;
ON_CALL(*mockSessionPtr, onDisconnect).WillByDefault([&sessionOnDisconnectSlots](auto slot) {
sessionOnDisconnectSlots.push_back(slot);
});
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
// repeat
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
auto const account1 = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(account1, sessionPtr);
// repeat
testFeedPtr->sub(account1, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
auto sessionPtr2 = std::make_shared<MockSession>();
auto mockSessionPtr2 = dynamic_cast<MockSession*>(sessionPtr2.get());
std::vector<web::SubscriptionContextInterface::OnDisconnectSlot> session2OnDisconnectSlots;
ON_CALL(*mockSessionPtr2, onDisconnect).WillByDefault([&session2OnDisconnectSlots](auto slot) {
session2OnDisconnectSlots.push_back(slot);
});
EXPECT_CALL(*mockSessionPtr2, onDisconnect);
testFeedPtr->sub(sessionPtr2);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 2);
auto const account2 = GetAccountIDWithString(ACCOUNT2);
EXPECT_CALL(*mockSessionPtr2, onDisconnect);
testFeedPtr->sub(account2, sessionPtr2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
EXPECT_CALL(*mockSessionPtr2, onDisconnect);
testFeedPtr->sub(account1, sessionPtr2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 3);
std::ranges::for_each(session2OnDisconnectSlots, [&sessionPtr2](auto& slot) { slot(sessionPtr2.get()); });
sessionPtr2.reset();
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 1);
std::ranges::for_each(sessionOnDisconnectSlots, [this](auto& slot) { slot(sessionPtr.get()); });
sessionPtr.reset();
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
EXPECT_EQ(testFeedPtr->transactionSubcount(), 0);
@@ -216,21 +256,9 @@ TEST_F(FeedProposedTransactionTest, AutoDisconnect)
struct ProposedTransactionFeedMockPrometheusTest : WithMockPrometheus, SyncExecutionCtxFixture {
protected:
std::shared_ptr<web::ConnectionBase> sessionPtr;
std::shared_ptr<ProposedTransactionFeed> testFeedPtr;
void
SetUp() override
{
testFeedPtr = std::make_shared<ProposedTransactionFeed>(ctx);
sessionPtr = std::make_shared<MockSession>();
}
void
TearDown() override
{
sessionPtr.reset();
testFeedPtr.reset();
}
web::SubscriptionContextPtr sessionPtr = std::make_shared<MockSession>();
std::shared_ptr<ProposedTransactionFeed> testFeedPtr = std::make_shared<ProposedTransactionFeed>(ctx);
MockSession* mockSessionPtr = dynamic_cast<MockSession*>(sessionPtr.get());
};
TEST_F(ProposedTransactionFeedMockPrometheusTest, subUnsub)
@@ -243,10 +271,12 @@ TEST_F(ProposedTransactionFeedMockPrometheusTest, subUnsub)
EXPECT_CALL(counterAccount, add(1));
EXPECT_CALL(counterAccount, add(-1));
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
testFeedPtr->unsub(sessionPtr);
auto const account = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(account, sessionPtr);
testFeedPtr->unsub(account, sessionPtr);
}
@@ -256,15 +286,24 @@ TEST_F(ProposedTransactionFeedMockPrometheusTest, AutoDisconnect)
auto& counterTx = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"tx_proposed\"}");
auto& counterAccount = makeMock<GaugeInt>("subscriptions_current_number", "{stream=\"account_proposed\"}");
std::vector<web::SubscriptionContextInterface::OnDisconnectSlot> sessionOnDisconnectSlots;
EXPECT_CALL(counterTx, add(1));
EXPECT_CALL(counterTx, add(-1));
EXPECT_CALL(counterAccount, add(1));
EXPECT_CALL(counterAccount, add(-1));
EXPECT_CALL(*mockSessionPtr, onDisconnect).WillOnce([&sessionOnDisconnectSlots](auto slot) {
sessionOnDisconnectSlots.push_back(slot);
});
testFeedPtr->sub(sessionPtr);
auto const account = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*mockSessionPtr, onDisconnect).WillOnce([&sessionOnDisconnectSlots](auto slot) {
sessionOnDisconnectSlots.push_back(slot);
});
testFeedPtr->sub(account, sessionPtr);
std::ranges::for_each(sessionOnDisconnectSlots, [this](auto& slot) { slot(sessionPtr.get()); });
sessionPtr.reset();
}

View File

@@ -24,7 +24,7 @@
#include "util/SyncExecutionCtxFixture.hpp"
#include "util/async/AnyExecutionContext.hpp"
#include "util/prometheus/Gauge.hpp"
#include "web/interface/ConnectionBase.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
@@ -38,23 +38,9 @@ using namespace util::prometheus;
struct FeedBaseMockPrometheusTest : WithMockPrometheus, SyncExecutionCtxFixture {
protected:
std::shared_ptr<web::ConnectionBase> sessionPtr;
std::shared_ptr<SingleFeedBase> testFeedPtr;
MockSession* mockSessionPtr = nullptr;
void
SetUp() override
{
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();
}
web::SubscriptionContextPtr sessionPtr = std::make_shared<MockSession>();
std::shared_ptr<SingleFeedBase> testFeedPtr = std::make_shared<SingleFeedBase>(ctx, "testFeed");
MockSession* mockSessionPtr = dynamic_cast<MockSession*>(sessionPtr.get());
};
TEST_F(FeedBaseMockPrometheusTest, subUnsub)
@@ -63,6 +49,7 @@ TEST_F(FeedBaseMockPrometheusTest, subUnsub)
EXPECT_CALL(counter, add(1));
EXPECT_CALL(counter, add(-1));
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
testFeedPtr->unsub(sessionPtr);
}
@@ -73,7 +60,10 @@ TEST_F(FeedBaseMockPrometheusTest, AutoUnsub)
EXPECT_CALL(counter, add(1));
EXPECT_CALL(counter, add(-1));
web::SubscriptionContextInterface::OnDisconnectSlot slot;
EXPECT_CALL(*mockSessionPtr, onDisconnect).WillOnce(testing::SaveArg<0>(&slot));
testFeedPtr->sub(sessionPtr);
slot(sessionPtr.get());
sessionPtr.reset();
}
@@ -88,7 +78,8 @@ using SingleFeedBaseTest = FeedBaseTest<NamedSingleFeedTest>;
TEST_F(SingleFeedBaseTest, Test)
{
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED))).Times(1);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED)));
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
testFeedPtr->pub(FEED);
@@ -100,17 +91,21 @@ TEST_F(SingleFeedBaseTest, Test)
TEST_F(SingleFeedBaseTest, TestAutoDisconnect)
{
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED))).Times(1);
web::SubscriptionContextInterface::OnDisconnectSlot slot;
EXPECT_CALL(*mockSessionPtr, onDisconnect).WillOnce(testing::SaveArg<0>(&slot));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(FEED)));
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
testFeedPtr->pub(FEED);
slot(sessionPtr.get());
sessionPtr.reset();
EXPECT_EQ(testFeedPtr->count(), 0);
}
TEST_F(SingleFeedBaseTest, RepeatSub)
{
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->count(), 1);
testFeedPtr->sub(sessionPtr);

View File

@@ -20,13 +20,14 @@
#include "data/Types.hpp"
#include "feed/FeedTestUtil.hpp"
#include "feed/SubscriptionManager.hpp"
#include "util/Assert.hpp"
#include "util/MockBackendTestFixture.hpp"
#include "util/MockPrometheus.hpp"
#include "util/MockWsBase.hpp"
#include "util/TestObject.hpp"
#include "util/async/context/BasicExecutionContext.hpp"
#include "util/async/context/SyncExecutionContext.hpp"
#include "web/interface/ConnectionBase.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
@@ -39,8 +40,8 @@
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/STObject.h>
#include <algorithm>
#include <memory>
#include <string>
#include <vector>
constexpr static auto ACCOUNT1 = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
@@ -56,25 +57,15 @@ using namespace feed::impl;
template <class Execution>
class SubscriptionManagerBaseTest : public util::prometheus::WithPrometheus, public MockBackendTest {
protected:
std::shared_ptr<SubscriptionManager> subscriptionManagerPtr;
std::shared_ptr<web::ConnectionBase> session;
MockSession* sessionPtr = nullptr;
void
SetUp() override
SubscriptionManagerBaseTest()
{
subscriptionManagerPtr = std::make_shared<SubscriptionManager>(Execution(2), backend);
session = std::make_shared<MockSession>();
session->apiSubVersion = 1;
sessionPtr = dynamic_cast<MockSession*>(session.get());
ASSERT(sessionPtr != nullptr, "dynamic_cast failed");
}
void
TearDown() override
{
session.reset();
subscriptionManagerPtr.reset();
}
std::shared_ptr<SubscriptionManager> subscriptionManagerPtr =
std::make_shared<SubscriptionManager>(Execution(2), backend);
web::SubscriptionContextPtr session = std::make_shared<MockSession>();
MockSession* sessionPtr = dynamic_cast<MockSession*>(session.get());
};
using SubscriptionManagerTest = SubscriptionManagerBaseTest<util::async::SyncExecutionContext>;
@@ -83,7 +74,9 @@ using SubscriptionManagerAsyncTest = SubscriptionManagerBaseTest<util::async::Po
TEST_F(SubscriptionManagerAsyncTest, MultipleThreadCtx)
{
EXPECT_CALL(*sessionPtr, onDisconnect);
subscriptionManagerPtr->subManifest(session);
EXPECT_CALL(*sessionPtr, onDisconnect);
subscriptionManagerPtr->subValidation(session);
constexpr static auto jsonManifest = R"({"manifest":"test"})";
@@ -97,7 +90,9 @@ TEST_F(SubscriptionManagerAsyncTest, MultipleThreadCtx)
TEST_F(SubscriptionManagerAsyncTest, MultipleThreadCtxSessionDieEarly)
{
EXPECT_CALL(*sessionPtr, onDisconnect);
subscriptionManagerPtr->subManifest(session);
EXPECT_CALL(*sessionPtr, onDisconnect);
subscriptionManagerPtr->subValidation(session);
EXPECT_CALL(*sessionPtr, send(testing::_)).Times(0);
@@ -121,8 +116,18 @@ TEST_F(SubscriptionManagerTest, ReportCurrentSubscriber)
"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>();
web::SubscriptionContextPtr const session1 = std::make_shared<MockSession>();
MockSession* mockSession1 = dynamic_cast<MockSession*>(session1.get());
web::SubscriptionContextPtr session2 = std::make_shared<MockSession>();
MockSession* mockSession2 = dynamic_cast<MockSession*>(session2.get());
std::vector<web::SubscriptionContextInterface::OnDisconnectSlot> session2OnDisconnectSlots;
ON_CALL(*mockSession2, onDisconnect).WillByDefault([&session2OnDisconnectSlots](auto slot) {
session2OnDisconnectSlots.push_back(slot);
});
EXPECT_CALL(*mockSession1, onDisconnect).Times(5);
EXPECT_CALL(*mockSession2, onDisconnect).Times(4);
subscriptionManagerPtr->subBookChanges(session1);
subscriptionManagerPtr->subBookChanges(session2);
subscriptionManagerPtr->subManifest(session1);
@@ -130,7 +135,10 @@ TEST_F(SubscriptionManagerTest, ReportCurrentSubscriber)
subscriptionManagerPtr->subProposedTransactions(session1);
subscriptionManagerPtr->subProposedTransactions(session2);
subscriptionManagerPtr->subTransactions(session1);
session2->apiSubVersion = 2;
// session2->apiSubVersion = 2;
EXPECT_CALL(*mockSession1, onDisconnect).Times(5);
EXPECT_CALL(*mockSession2, onDisconnect).Times(6);
subscriptionManagerPtr->subTransactions(session2);
subscriptionManagerPtr->subValidation(session1);
subscriptionManagerPtr->subValidation(session2);
@@ -172,6 +180,7 @@ TEST_F(SubscriptionManagerTest, ReportCurrentSubscriber)
checkResult(subscriptionManagerPtr->report(), 1);
// count down when session disconnect
std::ranges::for_each(session2OnDisconnectSlots, [&session2](auto& slot) { slot(session2.get()); });
session2.reset();
checkResult(subscriptionManagerPtr->report(), 0);
}
@@ -179,7 +188,8 @@ TEST_F(SubscriptionManagerTest, ReportCurrentSubscriber)
TEST_F(SubscriptionManagerTest, ManifestTest)
{
constexpr static auto dummyManifest = R"({"manifest":"test"})";
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(dummyManifest))).Times(1);
EXPECT_CALL(*sessionPtr, onDisconnect);
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(dummyManifest)));
subscriptionManagerPtr->subManifest(session);
subscriptionManagerPtr->forwardManifest(json::parse(dummyManifest).get_object());
@@ -191,7 +201,8 @@ TEST_F(SubscriptionManagerTest, ManifestTest)
TEST_F(SubscriptionManagerTest, ValidationTest)
{
constexpr static auto dummy = R"({"validation":"test"})";
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(dummy))).Times(1);
EXPECT_CALL(*sessionPtr, onDisconnect);
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(dummy)));
subscriptionManagerPtr->subValidation(session);
subscriptionManagerPtr->forwardValidation(json::parse(dummy).get_object());
@@ -202,6 +213,7 @@ TEST_F(SubscriptionManagerTest, ValidationTest)
TEST_F(SubscriptionManagerTest, BookChangesTest)
{
EXPECT_CALL(*sessionPtr, onDisconnect);
subscriptionManagerPtr->subBookChanges(session);
EXPECT_EQ(subscriptionManagerPtr->report()["book_changes"], 1);
@@ -234,7 +246,7 @@ TEST_F(SubscriptionManagerTest, BookChangesTest)
}
]
})";
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(bookChangePublish))).Times(1);
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(bookChangePublish)));
subscriptionManagerPtr->pubBookChanges(ledgerHeader, transactions);
@@ -266,6 +278,7 @@ TEST_F(SubscriptionManagerTest, LedgerTest)
})";
boost::asio::io_context ctx;
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
EXPECT_CALL(*sessionPtr, onDisconnect);
auto const res = subscriptionManagerPtr->subLedger(yield, session);
// check the response
EXPECT_EQ(res, json::parse(LedgerResponse));
@@ -289,7 +302,7 @@ TEST_F(SubscriptionManagerTest, LedgerTest)
"validated_ledgers":"10-31",
"txn_count":8
})";
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(ledgerPub))).Times(1);
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(ledgerPub)));
subscriptionManagerPtr->pubLedger(ledgerHeader2, fee2, "10-31", 8);
// test unsub
@@ -302,6 +315,7 @@ TEST_F(SubscriptionManagerTest, TransactionTest)
auto const issue1 = GetIssue(CURRENCY, ISSUER);
auto const account = GetAccountIDWithString(ISSUER);
ripple::Book const book{ripple::xrpIssue(), issue1};
EXPECT_CALL(*sessionPtr, onDisconnect).Times(3);
subscriptionManagerPtr->subBook(book, session);
subscriptionManagerPtr->subTransactions(session);
subscriptionManagerPtr->subAccount(account, session);
@@ -378,6 +392,7 @@ TEST_F(SubscriptionManagerTest, TransactionTest)
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
})";
EXPECT_CALL(*sessionPtr, send(SharedStringJsonEq(OrderbookPublish))).Times(3);
EXPECT_CALL(*sessionPtr, apiSubversion).Times(3).WillRepeatedly(testing::Return(1));
subscriptionManagerPtr->pubTransaction(trans1, ledgerHeader);
subscriptionManagerPtr->unsubBook(book, session);
@@ -391,6 +406,7 @@ TEST_F(SubscriptionManagerTest, TransactionTest)
TEST_F(SubscriptionManagerTest, ProposedTransactionTest)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*sessionPtr, onDisconnect).Times(4);
subscriptionManagerPtr->subProposedAccount(account, session);
subscriptionManagerPtr->subProposedTransactions(session);
EXPECT_EQ(subscriptionManagerPtr->report()["accounts_proposed"], 1);
@@ -476,6 +492,7 @@ TEST_F(SubscriptionManagerTest, ProposedTransactionTest)
auto const metaObj = CreateMetaDataForBookChange(CURRENCY, ACCOUNT1, 22, 3, 1, 1, 3);
trans1.metadata = metaObj.getSerializer().peekData();
EXPECT_CALL(*sessionPtr, apiSubversion).Times(2).WillRepeatedly(testing::Return(1));
subscriptionManagerPtr->pubTransaction(trans1, ledgerHeader);
// unsub account1
@@ -487,6 +504,7 @@ TEST_F(SubscriptionManagerTest, ProposedTransactionTest)
TEST_F(SubscriptionManagerTest, DuplicateResponseSubTxAndProposedTx)
{
EXPECT_CALL(*sessionPtr, onDisconnect).Times(3);
subscriptionManagerPtr->subProposedTransactions(session);
subscriptionManagerPtr->subTransactions(session);
EXPECT_EQ(subscriptionManagerPtr->report()["transactions"], 1);
@@ -502,6 +520,7 @@ TEST_F(SubscriptionManagerTest, DuplicateResponseSubTxAndProposedTx)
auto const metaObj = CreateMetaDataForBookChange(CURRENCY, ACCOUNT1, 22, 3, 1, 1, 3);
trans1.metadata = metaObj.getSerializer().peekData();
EXPECT_CALL(*sessionPtr, apiSubversion).Times(2).WillRepeatedly(testing::Return(1));
subscriptionManagerPtr->pubTransaction(trans1, ledgerHeader);
subscriptionManagerPtr->unsubTransactions(session);
@@ -513,12 +532,13 @@ TEST_F(SubscriptionManagerTest, DuplicateResponseSubTxAndProposedTx)
TEST_F(SubscriptionManagerTest, NoDuplicateResponseSubAccountAndProposedAccount)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*sessionPtr, onDisconnect).Times(3);
subscriptionManagerPtr->subProposedAccount(account, session);
subscriptionManagerPtr->subAccount(account, session);
EXPECT_EQ(subscriptionManagerPtr->report()["accounts_proposed"], 1);
EXPECT_EQ(subscriptionManagerPtr->report()["account"], 1);
EXPECT_CALL(*sessionPtr, send(testing::_)).Times(1);
EXPECT_CALL(*sessionPtr, send(testing::_));
auto const ledgerHeader = CreateLedgerHeader(LEDGERHASH, 33);
auto trans1 = TransactionAndMetadata();
@@ -528,6 +548,7 @@ TEST_F(SubscriptionManagerTest, NoDuplicateResponseSubAccountAndProposedAccount)
auto const metaObj = CreateMetaDataForBookChange(CURRENCY, ACCOUNT1, 22, 3, 1, 1, 3);
trans1.metadata = metaObj.getSerializer().peekData();
EXPECT_CALL(*sessionPtr, apiSubversion).WillRepeatedly(testing::Return(1));
subscriptionManagerPtr->pubTransaction(trans1, ledgerHeader);
// unsub account1

View File

@@ -20,7 +20,7 @@
#include "feed/impl/TrackableSignal.hpp"
#include "feed/impl/TrackableSignalMap.hpp"
#include "util/MockWsBase.hpp"
#include "web/interface/ConnectionBase.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include <gtest/gtest.h>
@@ -30,24 +30,12 @@
using namespace testing;
struct FeedTrackableSignalTests : Test {
protected:
std::shared_ptr<web::ConnectionBase> sessionPtr;
void
SetUp() override
{
sessionPtr = std::make_shared<MockSession>();
}
void
TearDown() override
{
}
web::SubscriptionContextPtr sessionPtr = std::make_shared<MockSession>();
};
TEST_F(FeedTrackableSignalTests, Connect)
{
feed::impl::TrackableSignal<web::ConnectionBase, std::string> signal;
feed::impl::TrackableSignal<web::SubscriptionContextInterface, std::string> signal;
std::string testString;
auto const slot = [&](std::string const& s) { testString += s; };
EXPECT_TRUE(signal.connectTrackableSlot(sessionPtr, slot));
@@ -69,7 +57,7 @@ TEST_F(FeedTrackableSignalTests, Connect)
TEST_F(FeedTrackableSignalTests, AutoDisconnect)
{
feed::impl::TrackableSignal<web::ConnectionBase, std::string> signal;
feed::impl::TrackableSignal<web::SubscriptionContextInterface, std::string> signal;
std::string testString;
auto const slot = [&](std::string const& s) { testString += s; };
EXPECT_TRUE(signal.connectTrackableSlot(sessionPtr, slot));
@@ -91,7 +79,7 @@ TEST_F(FeedTrackableSignalTests, AutoDisconnect)
TEST_F(FeedTrackableSignalTests, MapConnect)
{
feed::impl::TrackableSignalMap<std::string, web::ConnectionBase, std::string> signalMap;
feed::impl::TrackableSignalMap<std::string, web::SubscriptionContextInterface, std::string> signalMap;
std::string testString;
auto const slot = [&](std::string const& s) { testString += s; };
EXPECT_TRUE(signalMap.connectTrackableSlot(sessionPtr, "test", slot));
@@ -115,7 +103,7 @@ TEST_F(FeedTrackableSignalTests, MapConnect)
TEST_F(FeedTrackableSignalTests, MapAutoDisconnect)
{
feed::impl::TrackableSignalMap<std::string, web::ConnectionBase, std::string> signalMap;
feed::impl::TrackableSignalMap<std::string, web::SubscriptionContextInterface, std::string> signalMap;
std::string testString;
auto const slot = [&](std::string const& s) { testString += s; };
EXPECT_TRUE(signalMap.connectTrackableSlot(sessionPtr, "test", slot));

View File

@@ -25,7 +25,7 @@
#include "util/SyncExecutionCtxFixture.hpp"
#include "util/TestObject.hpp"
#include "util/prometheus/Gauge.hpp"
#include "web/interface/ConnectionBase.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
@@ -39,7 +39,9 @@
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TER.h>
#include <functional>
#include <memory>
#include <vector>
constexpr static auto ACCOUNT1 = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
@@ -164,6 +166,7 @@ using FeedTransactionTest = FeedBaseTest<TransactionFeed>;
TEST_F(FeedTransactionTest, SubTransactionV1)
{
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 1);
@@ -173,7 +176,9 @@ TEST_F(FeedTransactionTest, SubTransactionV1)
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1)));
testFeedPtr->pub(trans1, ledgerHeader, backend);
testFeedPtr->unsub(sessionPtr);
@@ -183,6 +188,7 @@ TEST_F(FeedTransactionTest, SubTransactionV1)
TEST_F(FeedTransactionTest, SubTransactionForProposedTx)
{
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->subProposed(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 0);
@@ -193,7 +199,8 @@ TEST_F(FeedTransactionTest, SubTransactionForProposedTx)
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1)));
testFeedPtr->pub(trans1, ledgerHeader, backend);
testFeedPtr->unsubProposed(sessionPtr);
@@ -202,7 +209,7 @@ TEST_F(FeedTransactionTest, SubTransactionForProposedTx)
TEST_F(FeedTransactionTest, SubTransactionV2)
{
sessionPtr->apiSubVersion = 2;
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 1);
@@ -213,7 +220,8 @@ TEST_F(FeedTransactionTest, SubTransactionV2)
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2)));
testFeedPtr->pub(trans1, ledgerHeader, backend);
testFeedPtr->unsub(sessionPtr);
@@ -225,6 +233,8 @@ TEST_F(FeedTransactionTest, SubTransactionV2)
TEST_F(FeedTransactionTest, SubAccountV1)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
@@ -236,7 +246,8 @@ TEST_F(FeedTransactionTest, SubAccountV1)
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1)));
testFeedPtr->pub(trans1, ledgerHeader, backend);
testFeedPtr->unsub(account, sessionPtr);
@@ -248,6 +259,8 @@ TEST_F(FeedTransactionTest, SubAccountV1)
TEST_F(FeedTransactionTest, SubForProposedAccount)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->subProposed(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
@@ -259,7 +272,8 @@ TEST_F(FeedTransactionTest, SubForProposedAccount)
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1)));
testFeedPtr->pub(trans1, ledgerHeader, backend);
testFeedPtr->unsubProposed(account, sessionPtr);
@@ -269,7 +283,7 @@ TEST_F(FeedTransactionTest, SubForProposedAccount)
TEST_F(FeedTransactionTest, SubAccountV2)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
sessionPtr->apiSubVersion = 2;
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
@@ -281,7 +295,8 @@ TEST_F(FeedTransactionTest, SubAccountV2)
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2)));
testFeedPtr->pub(trans1, ledgerHeader, backend);
testFeedPtr->unsub(account, sessionPtr);
@@ -293,7 +308,7 @@ TEST_F(FeedTransactionTest, SubAccountV2)
TEST_F(FeedTransactionTest, SubBothTransactionAndAccount)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
sessionPtr->apiSubVersion = 2;
EXPECT_CALL(*mockSessionPtr, onDisconnect).Times(2);
testFeedPtr->sub(account, sessionPtr);
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
@@ -307,6 +322,7 @@ TEST_F(FeedTransactionTest, SubBothTransactionAndAccount)
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, apiSubversion).Times(2).WillRepeatedly(testing::Return(2));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(2);
testFeedPtr->pub(trans1, ledgerHeader, backend);
@@ -322,6 +338,8 @@ TEST_F(FeedTransactionTest, SubBookV1)
{
auto const issue1 = GetIssue(CURRENCY, ISSUER);
ripple::Book const book{ripple::xrpIssue(), issue1};
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(book, sessionPtr);
EXPECT_EQ(testFeedPtr->bookSubCount(), 1);
@@ -394,6 +412,7 @@ TEST_F(FeedTransactionTest, SubBookV1)
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
})";
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(OrderbookPublish))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
@@ -448,6 +467,8 @@ TEST_F(FeedTransactionTest, SubBookV1)
"close_time_iso": "2000-01-01T00:00:00Z",
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
})";
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(OrderbookCancelPublish))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
@@ -504,6 +525,7 @@ TEST_F(FeedTransactionTest, SubBookV1)
metaObj = CreateMetaDataForCreateOffer(CURRENCY, ISSUER, 22, 3, 1);
trans1.metadata = metaObj.getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(OrderbookCreatePublish))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
@@ -517,7 +539,8 @@ TEST_F(FeedTransactionTest, SubBookV2)
{
auto const issue1 = GetIssue(CURRENCY, ISSUER);
ripple::Book const book{ripple::xrpIssue(), issue1};
sessionPtr->apiSubVersion = 2;
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(book, sessionPtr);
EXPECT_EQ(testFeedPtr->bookSubCount(), 1);
@@ -590,6 +613,7 @@ TEST_F(FeedTransactionTest, SubBookV2)
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
})";
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(OrderbookPublish))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
@@ -601,11 +625,13 @@ TEST_F(FeedTransactionTest, SubBookV2)
TEST_F(FeedTransactionTest, TransactionContainsBothAccountsSubed)
{
sessionPtr->apiSubVersion = 2;
auto const account = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(account, sessionPtr);
auto const account2 = GetAccountIDWithString(ACCOUNT2);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(account2, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
@@ -617,12 +643,14 @@ TEST_F(FeedTransactionTest, TransactionContainsBothAccountsSubed)
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
@@ -634,10 +662,13 @@ TEST_F(FeedTransactionTest, TransactionContainsBothAccountsSubed)
TEST_F(FeedTransactionTest, SubAccountRepeatWithDifferentVersion)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(account, sessionPtr);
auto const account2 = GetAccountIDWithString(ACCOUNT2);
sessionPtr->apiSubVersion = 2;
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(account2, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
@@ -649,12 +680,14 @@ TEST_F(FeedTransactionTest, SubAccountRepeatWithDifferentVersion)
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
testFeedPtr->unsub(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
@@ -667,10 +700,10 @@ TEST_F(FeedTransactionTest, SubAccountRepeatWithDifferentVersion)
TEST_F(FeedTransactionTest, SubTransactionRepeatWithDifferentVersion)
{
// sub version 1 first
sessionPtr->apiSubVersion = 1;
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
// sub version 2 later
sessionPtr->apiSubVersion = 2;
testFeedPtr->sub(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 1);
@@ -681,6 +714,7 @@ TEST_F(FeedTransactionTest, SubTransactionRepeatWithDifferentVersion)
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(2));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V2))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
@@ -693,9 +727,11 @@ TEST_F(FeedTransactionTest, SubTransactionRepeatWithDifferentVersion)
TEST_F(FeedTransactionTest, SubRepeat)
{
auto const session2 = std::make_shared<MockSession>();
session2->apiSubVersion = 1;
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
EXPECT_CALL(*session2, onDisconnect);
testFeedPtr->sub(session2);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 2);
@@ -712,7 +748,11 @@ TEST_F(FeedTransactionTest, SubRepeat)
auto const account = GetAccountIDWithString(ACCOUNT1);
auto const account2 = GetAccountIDWithString(ACCOUNT2);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(account, sessionPtr);
EXPECT_CALL(*session2, onDisconnect);
testFeedPtr->sub(account2, session2);
EXPECT_EQ(testFeedPtr->accountSubCount(), 2);
@@ -729,8 +769,12 @@ TEST_F(FeedTransactionTest, SubRepeat)
auto const issue1 = GetIssue(CURRENCY, ISSUER);
ripple::Book const book{ripple::xrpIssue(), issue1};
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(book, sessionPtr);
EXPECT_EQ(testFeedPtr->bookSubCount(), 1);
EXPECT_CALL(*session2, onDisconnect);
testFeedPtr->sub(book, session2);
EXPECT_EQ(testFeedPtr->bookSubCount(), 2);
@@ -744,6 +788,7 @@ TEST_F(FeedTransactionTest, SubRepeat)
TEST_F(FeedTransactionTest, PubTransactionWithOwnerFund)
{
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
auto const ledgerHeader = CreateLedgerHeader(LEDGERHASH, 33);
@@ -814,6 +859,7 @@ TEST_F(FeedTransactionTest, PubTransactionWithOwnerFund)
"engine_result_message":"The transaction was applied. Only final in a validated ledger."
})";
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TransactionForOwnerFund))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
}
@@ -856,6 +902,7 @@ constexpr static auto TRAN_FROZEN =
TEST_F(FeedTransactionTest, PubTransactionOfferCreationFrozenLine)
{
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
auto const ledgerHeader = CreateLedgerHeader(LEDGERHASH, 33);
@@ -888,12 +935,14 @@ TEST_F(FeedTransactionTest, PubTransactionOfferCreationFrozenLine)
ON_CALL(*backend, doFetchLedgerObject(kk, testing::_, testing::_))
.WillByDefault(testing::Return(accountRoot.getSerializer().peekData()));
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_FROZEN))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
}
TEST_F(FeedTransactionTest, SubTransactionOfferCreationGlobalFrozen)
{
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
auto const ledgerHeader = CreateLedgerHeader(LEDGERHASH, 33);
@@ -926,6 +975,7 @@ TEST_F(FeedTransactionTest, SubTransactionOfferCreationGlobalFrozen)
ON_CALL(*backend, doFetchLedgerObject(kk, testing::_, testing::_))
.WillByDefault(testing::Return(accountRoot.getSerializer().peekData()));
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_FROZEN))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
}
@@ -933,7 +983,11 @@ TEST_F(FeedTransactionTest, SubTransactionOfferCreationGlobalFrozen)
TEST_F(FeedTransactionTest, SubBothProposedAndValidatedAccount)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(account, sessionPtr);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->subProposed(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 1);
@@ -943,6 +997,8 @@ TEST_F(FeedTransactionTest, SubBothProposedAndValidatedAccount)
trans1.transaction = obj.getSerializer().peekData();
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
@@ -955,7 +1011,10 @@ TEST_F(FeedTransactionTest, SubBothProposedAndValidatedAccount)
TEST_F(FeedTransactionTest, SubBothProposedAndValidated)
{
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->subProposed(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 1);
@@ -966,6 +1025,7 @@ TEST_F(FeedTransactionTest, SubBothProposedAndValidated)
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, apiSubversion).Times(2).WillRepeatedly(testing::Return(1));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(2);
testFeedPtr->pub(trans1, ledgerHeader, backend);
@@ -976,6 +1036,7 @@ TEST_F(FeedTransactionTest, SubBothProposedAndValidated)
TEST_F(FeedTransactionTest, SubProposedDisconnect)
{
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->subProposed(sessionPtr);
EXPECT_EQ(testFeedPtr->transactionSubCount(), 0);
@@ -986,6 +1047,7 @@ TEST_F(FeedTransactionTest, SubProposedDisconnect)
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
@@ -996,6 +1058,8 @@ TEST_F(FeedTransactionTest, SubProposedDisconnect)
TEST_F(FeedTransactionTest, SubProposedAccountDisconnect)
{
auto const account = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->subProposed(account, sessionPtr);
EXPECT_EQ(testFeedPtr->accountSubCount(), 0);
@@ -1006,6 +1070,7 @@ TEST_F(FeedTransactionTest, SubProposedAccountDisconnect)
trans1.ledgerSequence = 32;
trans1.metadata = CreatePaymentTransactionMetaObject(ACCOUNT1, ACCOUNT2, 110, 30, 22).getSerializer().peekData();
EXPECT_CALL(*mockSessionPtr, apiSubversion).WillOnce(testing::Return(1));
EXPECT_CALL(*mockSessionPtr, send(SharedStringJsonEq(TRAN_V1))).Times(1);
testFeedPtr->pub(trans1, ledgerHeader, backend);
@@ -1015,21 +1080,9 @@ TEST_F(FeedTransactionTest, SubProposedAccountDisconnect)
struct TransactionFeedMockPrometheusTest : WithMockPrometheus, SyncExecutionCtxFixture {
protected:
std::shared_ptr<web::ConnectionBase> sessionPtr;
std::shared_ptr<TransactionFeed> testFeedPtr;
void
SetUp() override
{
testFeedPtr = std::make_shared<TransactionFeed>(ctx);
sessionPtr = std::make_shared<MockSession>();
}
void
TearDown() override
{
sessionPtr.reset();
testFeedPtr.reset();
}
web::SubscriptionContextPtr sessionPtr = std::make_shared<MockSession>();
std::shared_ptr<TransactionFeed> testFeedPtr = std::make_shared<TransactionFeed>(ctx);
MockSession* mockSessionPtr = dynamic_cast<MockSession*>(sessionPtr.get());
};
TEST_F(TransactionFeedMockPrometheusTest, subUnsub)
@@ -1045,15 +1098,18 @@ TEST_F(TransactionFeedMockPrometheusTest, subUnsub)
EXPECT_CALL(counterBook, add(1));
EXPECT_CALL(counterBook, add(-1));
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(sessionPtr);
testFeedPtr->unsub(sessionPtr);
auto const account = GetAccountIDWithString(ACCOUNT1);
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(account, sessionPtr);
testFeedPtr->unsub(account, sessionPtr);
auto const issue1 = GetIssue(CURRENCY, ISSUER);
ripple::Book const book{ripple::xrpIssue(), issue1};
EXPECT_CALL(*mockSessionPtr, onDisconnect);
testFeedPtr->sub(book, sessionPtr);
testFeedPtr->unsub(book, sessionPtr);
}
@@ -1071,6 +1127,11 @@ TEST_F(TransactionFeedMockPrometheusTest, AutoDisconnect)
EXPECT_CALL(counterBook, add(1));
EXPECT_CALL(counterBook, add(-1));
std::vector<web::SubscriptionContextInterface::OnDisconnectSlot> onDisconnectSlots;
EXPECT_CALL(*mockSessionPtr, onDisconnect).Times(3).WillRepeatedly([&onDisconnectSlots](auto const& slot) {
onDisconnectSlots.push_back(slot);
});
testFeedPtr->sub(sessionPtr);
auto const account = GetAccountIDWithString(ACCOUNT1);
@@ -1080,5 +1141,9 @@ TEST_F(TransactionFeedMockPrometheusTest, AutoDisconnect)
ripple::Book const book{ripple::xrpIssue(), issue1};
testFeedPtr->sub(book, sessionPtr);
// Emulate onDisconnect signal is called
for (auto const& slot : onDisconnectSlots)
slot(sessionPtr.get());
sessionPtr.reset();
}

View File

@@ -28,7 +28,7 @@
#include "util/MockWsBase.hpp"
#include "util/NameGenerator.hpp"
#include "util/TestObject.hpp"
#include "web/interface/ConnectionBase.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
@@ -63,21 +63,9 @@ constexpr static auto PAYS20XRPGETS10USDBOOKDIR = "7B1767D41DBCE79D9585CF9D0262A
constexpr static auto INDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
constexpr static auto INDEX2 = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
class RPCSubscribeHandlerTest : public HandlerBaseTest {
protected:
void
SetUp() override
{
HandlerBaseTest::SetUp();
session_ = std::make_shared<MockSession>();
}
void
TearDown() override
{
HandlerBaseTest::TearDown();
}
std::shared_ptr<web::ConnectionBase> session_;
struct RPCSubscribeHandlerTest : HandlerBaseTest {
web::SubscriptionContextPtr session_ = std::make_shared<MockSession>();
MockSession* mockSession_ = dynamic_cast<MockSession*>(session_.get());
StrictMockSubscriptionManagerSharedPtr mockSubscriptionManagerPtr;
};
@@ -578,6 +566,7 @@ TEST_F(RPCSubscribeHandlerTest, EmptyResponse)
{
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
EXPECT_CALL(*mockSession_, setApiSubversion(0));
auto const output = handler.process(json::parse(R"({})"), Context{yield, session_});
ASSERT_TRUE(output);
EXPECT_TRUE(output.result->as_object().empty());
@@ -594,12 +583,13 @@ TEST_F(RPCSubscribeHandlerTest, StreamsWithoutLedger)
);
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
EXPECT_CALL(*mockSubscriptionManagerPtr, subTransactions).Times(1);
EXPECT_CALL(*mockSubscriptionManagerPtr, subValidation).Times(1);
EXPECT_CALL(*mockSubscriptionManagerPtr, subManifest).Times(1);
EXPECT_CALL(*mockSubscriptionManagerPtr, subBookChanges).Times(1);
EXPECT_CALL(*mockSubscriptionManagerPtr, subProposedTransactions).Times(1);
EXPECT_CALL(*mockSubscriptionManagerPtr, subTransactions);
EXPECT_CALL(*mockSubscriptionManagerPtr, subValidation);
EXPECT_CALL(*mockSubscriptionManagerPtr, subManifest);
EXPECT_CALL(*mockSubscriptionManagerPtr, subBookChanges);
EXPECT_CALL(*mockSubscriptionManagerPtr, subProposedTransactions);
EXPECT_CALL(*mockSession_, setApiSubversion(0));
auto const output = handler.process(input, Context{yield, session_});
ASSERT_TRUE(output);
EXPECT_TRUE(output.result->as_object().empty());
@@ -630,6 +620,7 @@ TEST_F(RPCSubscribeHandlerTest, StreamsLedger)
EXPECT_CALL(*mockSubscriptionManagerPtr, subLedger)
.WillOnce(testing::Return(boost::json::parse(expectedOutput).as_object()));
EXPECT_CALL(*mockSession_, setApiSubversion(0));
auto const output = handler.process(input, Context{yield, session_});
ASSERT_TRUE(output);
EXPECT_EQ(output.result->as_object(), json::parse(expectedOutput));
@@ -649,8 +640,9 @@ TEST_F(RPCSubscribeHandlerTest, Accounts)
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
EXPECT_CALL(*mockSubscriptionManagerPtr, subAccount(GetAccountIDWithString(ACCOUNT), session_)).Times(1);
EXPECT_CALL(*mockSubscriptionManagerPtr, subAccount(GetAccountIDWithString(ACCOUNT), session_));
EXPECT_CALL(*mockSubscriptionManagerPtr, subAccount(GetAccountIDWithString(ACCOUNT2), session_)).Times(2);
EXPECT_CALL(*mockSession_, setApiSubversion(0));
auto const output = handler.process(input, Context{yield, session_});
ASSERT_TRUE(output);
EXPECT_TRUE(output.result->as_object().empty());
@@ -670,10 +662,10 @@ TEST_F(RPCSubscribeHandlerTest, AccountsProposed)
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
EXPECT_CALL(*mockSubscriptionManagerPtr, subProposedAccount(GetAccountIDWithString(ACCOUNT), session_))
.Times(1);
EXPECT_CALL(*mockSubscriptionManagerPtr, subProposedAccount(GetAccountIDWithString(ACCOUNT), session_));
EXPECT_CALL(*mockSubscriptionManagerPtr, subProposedAccount(GetAccountIDWithString(ACCOUNT2), session_))
.Times(2);
EXPECT_CALL(*mockSession_, setApiSubversion(0));
auto const output = handler.process(input, Context{yield, session_});
ASSERT_TRUE(output);
EXPECT_TRUE(output.result->as_object().empty());
@@ -703,7 +695,8 @@ TEST_F(RPCSubscribeHandlerTest, JustBooks)
));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
EXPECT_CALL(*mockSubscriptionManagerPtr, subBook).Times(1);
EXPECT_CALL(*mockSubscriptionManagerPtr, subBook);
EXPECT_CALL(*mockSession_, setApiSubversion(0));
auto const output = handler.process(input, Context{yield, session_});
ASSERT_TRUE(output);
EXPECT_TRUE(output.result->as_object().empty());
@@ -735,6 +728,7 @@ TEST_F(RPCSubscribeHandlerTest, BooksBothSet)
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
EXPECT_CALL(*mockSubscriptionManagerPtr, subBook).Times(2);
EXPECT_CALL(*mockSession_, setApiSubversion(0));
auto const output = handler.process(input, Context{yield, session_});
ASSERT_TRUE(output);
EXPECT_TRUE(output.result->as_object().empty());
@@ -899,6 +893,7 @@ TEST_F(RPCSubscribeHandlerTest, BooksBothSnapshotSet)
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
EXPECT_CALL(*mockSubscriptionManagerPtr, subBook).Times(2);
EXPECT_CALL(*mockSession_, setApiSubversion(0));
auto const output = handler.process(input, Context{yield, session_});
ASSERT_TRUE(output);
EXPECT_EQ(output.result->as_object().at("bids").as_array().size(), 10);
@@ -1007,7 +1002,7 @@ TEST_F(RPCSubscribeHandlerTest, BooksBothUnsetSnapshotSet)
std::vector<Blob> const bbs2(10, gets10USDPays20XRPOffer.getSerializer().peekData());
ON_CALL(*backend, doFetchLedgerObjects(indexes2, MAXSEQ, _)).WillByDefault(Return(bbs2));
EXPECT_CALL(*backend, doFetchLedgerObjects).Times(1);
EXPECT_CALL(*backend, doFetchLedgerObjects);
static auto const expectedOffer = fmt::format(
R"({{
@@ -1038,7 +1033,8 @@ TEST_F(RPCSubscribeHandlerTest, BooksBothUnsetSnapshotSet)
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
EXPECT_CALL(*mockSubscriptionManagerPtr, subBook).Times(1);
EXPECT_CALL(*mockSubscriptionManagerPtr, subBook);
EXPECT_CALL(*mockSession_, setApiSubversion(0));
auto const output = handler.process(input, Context{yield, session_});
ASSERT_TRUE(output);
EXPECT_EQ(output.result->as_object().at("offers").as_array().size(), 10);
@@ -1056,11 +1052,12 @@ TEST_F(RPCSubscribeHandlerTest, APIVersion)
auto const apiVersion = 2;
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{SubscribeHandler{backend, mockSubscriptionManagerPtr}};
EXPECT_CALL(*mockSubscriptionManagerPtr, subProposedTransactions).Times(1);
EXPECT_CALL(*mockSubscriptionManagerPtr, subProposedTransactions);
EXPECT_CALL(*mockSession_, setApiSubversion(apiVersion));
auto const output =
handler.process(input, Context{.yield = yield, .session = session_, .apiVersion = apiVersion});
ASSERT_TRUE(output);
EXPECT_EQ(session_->apiSubVersion, apiVersion);
// EXPECT_EQ(session_->apiSubVersion, apiVersion);
});
}

View File

@@ -26,7 +26,7 @@
#include "util/MockSubscriptionManager.hpp"
#include "util/MockWsBase.hpp"
#include "util/NameGenerator.hpp"
#include "web/interface/ConnectionBase.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include <boost/json/parse.hpp>
#include <boost/json/value.hpp>
@@ -49,19 +49,7 @@ constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
struct RPCUnsubscribeTest : HandlerBaseTest {
void
SetUp() override
{
HandlerBaseTest::SetUp();
session_ = std::make_shared<MockSession>();
}
void
TearDown() override
{
HandlerBaseTest::TearDown();
}
std::shared_ptr<web::ConnectionBase> session_;
web::SubscriptionContextPtr session_ = std::make_shared<MockSession>();
StrictMockSubscriptionManagerSharedPtr mockSubscriptionManagerPtr;
};

View File

@@ -155,10 +155,12 @@ TEST_F(CoroutineGroupTests, TooManyCoroutines)
}));
EXPECT_FALSE(group.spawn(yield, [this](boost::asio::yield_context) { callback2_.Call(); }));
EXPECT_TRUE(group.isFull());
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{2}};
timer.async_wait(yield);
EXPECT_FALSE(group.isFull());
EXPECT_TRUE(group.spawn(yield, [this](boost::asio::yield_context) { callback2_.Call(); }));
group.asyncWait(yield);
@@ -166,16 +168,28 @@ TEST_F(CoroutineGroupTests, TooManyCoroutines)
});
}
TEST_F(CoroutineGroupTests, CanSpawn)
TEST_F(CoroutineGroupTests, SpawnForeign)
{
EXPECT_CALL(callback1_, Call);
testing::Sequence const sequence;
EXPECT_CALL(callback1_, Call).InSequence(sequence);
EXPECT_CALL(callback2_, Call).InSequence(sequence);
runSpawn([this](boost::asio::yield_context yield) {
CoroutineGroup group{yield, 1};
EXPECT_TRUE(group.canSpawn());
group.spawn(yield, [&group, this](boost::asio::yield_context) {
auto const onForeignComplete = group.registerForeign();
[&]() { ASSERT_TRUE(onForeignComplete.has_value()); }();
[&]() { ASSERT_FALSE(group.registerForeign().has_value()); }();
boost::asio::spawn(ctx, [this, &onForeignComplete](boost::asio::yield_context innerYield) {
boost::asio::steady_timer timer{innerYield.get_executor(), std::chrono::milliseconds{2}};
timer.async_wait(innerYield);
callback1_.Call();
EXPECT_FALSE(group.canSpawn());
onForeignComplete->operator()();
});
group.asyncWait(yield);
callback2_.Call();
});
}

View File

@@ -18,8 +18,9 @@
//==============================================================================
#include "util/LoggerFixtures.hpp"
#include "util/NameGenerator.hpp"
#include "util/config/Config.hpp"
#include "web/impl/AdminVerificationStrategy.hpp"
#include "web/AdminVerificationStrategy.hpp"
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
@@ -34,7 +35,7 @@ namespace http = boost::beast::http;
class IPAdminVerificationStrategyTest : public NoLoggerFixture {
protected:
web::impl::IPAdminVerificationStrategy strat_;
web::IPAdminVerificationStrategy strat_;
http::request<http::string_body> request_;
};
@@ -52,7 +53,7 @@ protected:
std::string const password_ = "secret";
std::string const passwordHash_ = "2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b";
web::impl::PasswordAdminVerificationStrategy strat_{password_};
web::PasswordAdminVerificationStrategy strat_{password_};
static http::request<http::string_body>
makeRequest(std::string const& password, http::field const field = http::field::authorization)
@@ -92,10 +93,10 @@ class MakeAdminVerificationStrategyTest : public testing::TestWithParam<MakeAdmi
TEST_P(MakeAdminVerificationStrategyTest, ChoosesStrategyCorrectly)
{
auto strat = web::impl::make_AdminVerificationStrategy(GetParam().passwordOpt);
auto ipStrat = dynamic_cast<web::impl::IPAdminVerificationStrategy*>(strat.get());
auto strat = web::make_AdminVerificationStrategy(GetParam().passwordOpt);
auto ipStrat = dynamic_cast<web::IPAdminVerificationStrategy*>(strat.get());
EXPECT_EQ(ipStrat != nullptr, GetParam().expectIpStrategy);
auto passwordStrat = dynamic_cast<web::impl::PasswordAdminVerificationStrategy*>(strat.get());
auto passwordStrat = dynamic_cast<web::PasswordAdminVerificationStrategy*>(strat.get());
EXPECT_EQ(passwordStrat != nullptr, GetParam().expectPasswordStrategy);
}
@@ -136,10 +137,8 @@ struct MakeAdminVerificationStrategyFromConfigTest
TEST_P(MakeAdminVerificationStrategyFromConfigTest, ChecksConfig)
{
util::Config const serverConfig{boost::json::parse(GetParam().config)};
auto const result = web::impl::make_AdminVerificationStrategy(serverConfig);
if (GetParam().expectedError) {
EXPECT_FALSE(result.has_value());
}
auto const result = web::make_AdminVerificationStrategy(serverConfig);
EXPECT_EQ(not result.has_value(), GetParam().expectedError);
}
INSTANTIATE_TEST_SUITE_P(
@@ -149,32 +148,33 @@ INSTANTIATE_TEST_SUITE_P(
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "NoPasswordNoLocalAdmin",
.config = "{}",
.expectedError = true
.expectedError = false
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "OnlyPassword",
.config = R"({"admin_password": "password"})",
.config = R"({"server": {"admin_password": "password"}})",
.expectedError = false
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "OnlyLocalAdmin",
.config = R"({"local_admin": true})",
.config = R"({"server": {"local_admin": true}})",
.expectedError = false
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "OnlyLocalAdminDisabled",
.config = R"({"local_admin": false})",
.config = R"({"server": {"local_admin": false}})",
.expectedError = true
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "LocalAdminAndPassword",
.config = R"({"local_admin": true, "admin_password": "password"})",
.config = R"({"server": {"local_admin": true, "admin_password": "password"}})",
.expectedError = true
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "LocalAdminDisabledAndPassword",
.config = R"({"local_admin": false, "admin_password": "password"})",
.config = R"({"server": {"local_admin": false, "admin_password": "password"}})",
.expectedError = false
}
)
),
tests::util::NameGenerator
);

View File

@@ -26,6 +26,7 @@
#include "util/Taggable.hpp"
#include "util/config/Config.hpp"
#include "web/RPCServerHandler.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/interface/ConnectionBase.hpp"
#include <boost/beast/http/status.hpp>
@@ -61,28 +62,25 @@ struct MockWsBase : public web::ConnectionBase {
lastStatus = status;
}
SubscriptionContextPtr
makeSubscriptionContext(util::TagDecoratorFactory const&) override
{
return {};
}
MockWsBase(util::TagDecoratorFactory const& factory) : web::ConnectionBase(factory, "localhost.fake.ip")
{
}
};
struct WebRPCServerHandlerTest : util::prometheus::WithPrometheus, MockBackendTest, SyncAsioContextTest {
void
SetUp() override
{
etl = std::make_shared<MockETLService>();
rpcEngine = std::make_shared<MockAsyncRPCEngine>();
tagFactory = std::make_shared<util::TagDecoratorFactory>(cfg);
session = std::make_shared<MockWsBase>(*tagFactory);
handler = std::make_shared<RPCServerHandler<MockAsyncRPCEngine, MockETLService>>(cfg, backend, rpcEngine, etl);
}
std::shared_ptr<MockAsyncRPCEngine> rpcEngine;
std::shared_ptr<MockETLService> etl;
std::shared_ptr<util::TagDecoratorFactory> tagFactory;
std::shared_ptr<RPCServerHandler<MockAsyncRPCEngine, MockETLService>> handler;
std::shared_ptr<MockWsBase> session;
util::Config cfg;
std::shared_ptr<MockAsyncRPCEngine> rpcEngine = std::make_shared<MockAsyncRPCEngine>();
std::shared_ptr<MockETLService> etl = std::make_shared<MockETLService>();
std::shared_ptr<util::TagDecoratorFactory> tagFactory = std::make_shared<util::TagDecoratorFactory>(cfg);
std::shared_ptr<RPCServerHandler<MockAsyncRPCEngine, MockETLService>> handler =
std::make_shared<RPCServerHandler<MockAsyncRPCEngine, MockETLService>>(cfg, backend, rpcEngine, etl);
std::shared_ptr<MockWsBase> session = std::make_shared<MockWsBase>(*tagFactory);
};
TEST_F(WebRPCServerHandlerTest, HTTPDefaultPath)
@@ -524,7 +522,7 @@ TEST_F(WebRPCServerHandlerTest, HTTPBadSyntaxWhenRequestSubscribe)
"result": {
"error": "badSyntax",
"error_code": 1,
"error_message": "Subscribe and unsubscribe are only allowed or websocket.",
"error_message": "Subscribe and unsubscribe are only allowed for websocket.",
"status": "error",
"type": "response",
"request": {

View File

@@ -26,12 +26,12 @@
#include "util/config/Config.hpp"
#include "util/prometheus/Label.hpp"
#include "util/prometheus/Prometheus.hpp"
#include "web/AdminVerificationStrategy.hpp"
#include "web/Server.hpp"
#include "web/dosguard/DOSGuard.hpp"
#include "web/dosguard/DOSGuardInterface.hpp"
#include "web/dosguard/IntervalSweepHandler.hpp"
#include "web/dosguard/WhitelistHandler.hpp"
#include "web/impl/AdminVerificationStrategy.hpp"
#include "web/interface/ConnectionBase.hpp"
#include <boost/asio/io_context.hpp>

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 "util/LoggerFixtures.hpp"
#include "util/Taggable.hpp"
#include "util/config/Config.hpp"
#include "web/SubscriptionContext.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/interface/ConnectionBaseMock.hpp"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
#include <string>
using namespace web;
struct SubscriptionContextTests : NoLoggerFixture {
util::TagDecoratorFactory tagFactory_{util::Config{}};
ConnectionBaseStrictMockPtr connection_ =
std::make_shared<testing::StrictMock<ConnectionBaseMock>>(tagFactory_, "some ip");
SubscriptionContext subscriptionContext_{tagFactory_, connection_};
testing::StrictMock<testing::MockFunction<void(SubscriptionContextInterface*)>> callbackMock_;
};
TEST_F(SubscriptionContextTests, send)
{
auto message = std::make_shared<std::string>("message");
EXPECT_CALL(*connection_, send(message));
subscriptionContext_.send(message);
}
TEST_F(SubscriptionContextTests, sendConnectionExpired)
{
auto message = std::make_shared<std::string>("message");
connection_.reset();
subscriptionContext_.send(message);
}
TEST_F(SubscriptionContextTests, onDisconnect)
{
auto localContext = std::make_unique<SubscriptionContext>(tagFactory_, connection_);
localContext->onDisconnect(callbackMock_.AsStdFunction());
EXPECT_CALL(callbackMock_, Call(localContext.get()));
localContext.reset();
}
TEST_F(SubscriptionContextTests, setApiSubversion)
{
EXPECT_EQ(subscriptionContext_.apiSubversion(), 0);
subscriptionContext_.setApiSubversion(42);
EXPECT_EQ(subscriptionContext_.apiSubversion(), 42);
}

View File

@@ -0,0 +1,288 @@
//------------------------------------------------------------------------------
/*
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 "rpc/Errors.hpp"
#include "util/LoggerFixtures.hpp"
#include "util/NameGenerator.hpp"
#include "util/Taggable.hpp"
#include "util/config/Config.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/impl/ErrorHandling.hpp"
#include "web/interface/ConnectionBase.hpp"
#include "web/interface/ConnectionBaseMock.hpp"
#include <boost/beast/http/status.hpp>
#include <boost/json/object.hpp>
#include <boost/json/serialize.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
#include <optional>
#include <string>
using namespace web::impl;
using namespace web;
struct ErrorHandlingTests : NoLoggerFixture {
util::TagDecoratorFactory tagFactory_{util::Config{}};
std::string const clientIp_ = "some ip";
ConnectionBaseStrictMockPtr connection_ =
std::make_shared<testing::StrictMock<ConnectionBaseMock>>(tagFactory_, clientIp_);
};
struct ErrorHandlingComposeErrorTestBundle {
std::string testName;
bool connectionUpgraded;
std::optional<boost::json::object> request;
boost::json::object expectedResult;
};
struct ErrorHandlingComposeErrorTest : ErrorHandlingTests,
testing::WithParamInterface<ErrorHandlingComposeErrorTestBundle> {};
TEST_P(ErrorHandlingComposeErrorTest, composeError)
{
connection_->upgraded = GetParam().connectionUpgraded;
ErrorHelper errorHelper{connection_, GetParam().request};
auto const result = errorHelper.composeError(rpc::RippledError::rpcNOT_READY);
EXPECT_EQ(boost::json::serialize(result), boost::json::serialize(GetParam().expectedResult));
}
INSTANTIATE_TEST_CASE_P(
ErrorHandlingComposeErrorTestGroup,
ErrorHandlingComposeErrorTest,
testing::ValuesIn(
{ErrorHandlingComposeErrorTestBundle{
"NoRequest_UpgradedConnection",
true,
std::nullopt,
{{"error", "notReady"},
{"error_code", 13},
{"error_message", "Not ready to handle this request."},
{"status", "error"},
{"type", "response"}}
},
ErrorHandlingComposeErrorTestBundle{
"NoRequest_NotUpgradedConnection",
false,
std::nullopt,
{{"result",
{{"error", "notReady"},
{"error_code", 13},
{"error_message", "Not ready to handle this request."},
{"status", "error"},
{"type", "response"}}}}
},
ErrorHandlingComposeErrorTestBundle{
"Request_UpgradedConnection",
true,
boost::json::object{{"id", 1}, {"api_version", 2}},
{{"error", "notReady"},
{"error_code", 13},
{"error_message", "Not ready to handle this request."},
{"status", "error"},
{"type", "response"},
{"id", 1},
{"api_version", 2},
{"request", {{"id", 1}, {"api_version", 2}}}}
},
ErrorHandlingComposeErrorTestBundle{
"Request_NotUpgradedConnection",
false,
boost::json::object{{"id", 1}, {"api_version", 2}},
{{"result",
{{"error", "notReady"},
{"error_code", 13},
{"error_message", "Not ready to handle this request."},
{"status", "error"},
{"type", "response"},
{"id", 1},
{"request", {{"id", 1}, {"api_version", 2}}}}}}
}}
),
tests::util::NameGenerator
);
struct ErrorHandlingSendErrorTestBundle {
std::string testName;
bool connectionUpgraded;
rpc::Status status;
std::string expectedMessage;
boost::beast::http::status expectedStatus;
};
struct ErrorHandlingSendErrorTest : ErrorHandlingTests,
testing::WithParamInterface<ErrorHandlingSendErrorTestBundle> {};
TEST_P(ErrorHandlingSendErrorTest, sendError)
{
connection_->upgraded = GetParam().connectionUpgraded;
ErrorHelper errorHelper{connection_};
EXPECT_CALL(*connection_, send(std::string{GetParam().expectedMessage}, GetParam().expectedStatus));
errorHelper.sendError(GetParam().status);
}
INSTANTIATE_TEST_CASE_P(
ErrorHandlingSendErrorTestGroup,
ErrorHandlingSendErrorTest,
testing::ValuesIn({
ErrorHandlingSendErrorTestBundle{
"UpgradedConnection",
true,
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})",
boost::beast::http::status::ok
},
ErrorHandlingSendErrorTestBundle{
"NotUpgradedConnection_InvalidApiVersion",
false,
rpc::Status{rpc::ClioError::rpcINVALID_API_VERSION},
"invalid_API_version",
boost::beast::http::status::bad_request
},
ErrorHandlingSendErrorTestBundle{
"NotUpgradedConnection_CommandIsMissing",
false,
rpc::Status{rpc::ClioError::rpcCOMMAND_IS_MISSING},
"Null method",
boost::beast::http::status::bad_request
},
ErrorHandlingSendErrorTestBundle{
"NotUpgradedConnection_CommandIsEmpty",
false,
rpc::Status{rpc::ClioError::rpcCOMMAND_IS_EMPTY},
"method is empty",
boost::beast::http::status::bad_request
},
ErrorHandlingSendErrorTestBundle{
"NotUpgradedConnection_CommandNotString",
false,
rpc::Status{rpc::ClioError::rpcCOMMAND_NOT_STRING},
"method is not string",
boost::beast::http::status::bad_request
},
ErrorHandlingSendErrorTestBundle{
"NotUpgradedConnection_ParamsUnparseable",
false,
rpc::Status{rpc::ClioError::rpcPARAMS_UNPARSEABLE},
"params unparseable",
boost::beast::http::status::bad_request
},
ErrorHandlingSendErrorTestBundle{
"NotUpgradedConnection_RippledError",
false,
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
R"({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})",
boost::beast::http::status::bad_request
},
}),
tests::util::NameGenerator
);
TEST_F(ErrorHandlingTests, sendInternalError)
{
ErrorHelper errorHelper{connection_};
EXPECT_CALL(
*connection_,
send(
std::string{
R"({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"}})"
},
boost::beast::http::status::internal_server_error
)
);
errorHelper.sendInternalError();
}
TEST_F(ErrorHandlingTests, sendNotReadyError)
{
ErrorHelper errorHelper{connection_};
EXPECT_CALL(
*connection_,
send(
std::string{
R"({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})"
},
boost::beast::http::status::ok
)
);
errorHelper.sendNotReadyError();
}
TEST_F(ErrorHandlingTests, sendTooBusyError_UpgradedConnection)
{
connection_->upgraded = true;
ErrorHelper errorHelper{connection_};
EXPECT_CALL(
*connection_,
send(
std::string{
R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})"
},
boost::beast::http::status::ok
)
);
errorHelper.sendTooBusyError();
}
TEST_F(ErrorHandlingTests, sendTooBusyError_NotUpgradedConnection)
{
connection_->upgraded = false;
ErrorHelper errorHelper{connection_};
EXPECT_CALL(
*connection_,
send(
std::string{
R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})"
},
boost::beast::http::status::service_unavailable
)
);
errorHelper.sendTooBusyError();
}
TEST_F(ErrorHandlingTests, sendJsonParsingError_UpgradedConnection)
{
connection_->upgraded = true;
ErrorHelper errorHelper{connection_};
EXPECT_CALL(
*connection_,
send(
std::string{
R"({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})"
},
boost::beast::http::status::ok
)
);
errorHelper.sendJsonParsingError();
}
TEST_F(ErrorHandlingTests, sendJsonParsingError_NotUpgradedConnection)
{
connection_->upgraded = false;
ErrorHelper errorHelper{connection_};
EXPECT_CALL(
*connection_,
send(std::string{"Unable to parse JSON from the request"}, boost::beast::http::status::bad_request)
);
errorHelper.sendJsonParsingError();
}

View File

@@ -0,0 +1,441 @@
//------------------------------------------------------------------------------
/*
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 "rpc/Errors.hpp"
#include "rpc/common/Types.hpp"
#include "util/AsioContextTestFixture.hpp"
#include "util/MockBackendTestFixture.hpp"
#include "util/MockETLService.hpp"
#include "util/MockPrometheus.hpp"
#include "util/MockRPCEngine.hpp"
#include "util/Taggable.hpp"
#include "util/config/Config.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/ng/MockConnection.hpp"
#include "web/ng/RPCServerHandler.hpp"
#include "web/ng/Request.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <cstdint>
#include <iterator>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_set>
#include <utility>
using namespace web::ng;
using testing::Return;
using testing::StrictMock;
namespace http = boost::beast::http;
struct ng_RPCServerHandlerTest : util::prometheus::WithPrometheus, MockBackendTestStrict, SyncAsioContextTest {
std::shared_ptr<testing::StrictMock<MockRPCEngine>> rpcEngine_ =
std::make_shared<testing::StrictMock<MockRPCEngine>>();
std::shared_ptr<StrictMock<MockETLService>> etl_ = std::make_shared<StrictMock<MockETLService>>();
RPCServerHandler<MockRPCEngine, MockETLService> rpcServerHandler_{util::Config{}, backend, rpcEngine_, etl_};
util::TagDecoratorFactory tagFactory_{util::Config{}};
StrictMockConnectionMetadata connectionMetadata_{"some ip", tagFactory_};
static Request
makeHttpRequest(std::string_view body)
{
return Request{http::request<http::string_body>{http::verb::post, "/", 11, body}};
}
};
TEST_F(ng_RPCServerHandlerTest, PostToRpcEngineFailed)
{
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest("some message");
EXPECT_CALL(*rpcEngine_, post).WillOnce(Return(false));
EXPECT_CALL(*rpcEngine_, notifyTooBusy());
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::service_unavailable);
});
}
TEST_F(ng_RPCServerHandlerTest, CoroutineSleepsUntilRpcEngineFinishes)
{
StrictMock<testing::MockFunction<void()>> rpcServerHandlerDone;
StrictMock<testing::MockFunction<void()>> rpcEngineDone;
testing::Expectation const expectedRpcEngineDone = EXPECT_CALL(rpcEngineDone, Call);
EXPECT_CALL(rpcServerHandlerDone, Call).After(expectedRpcEngineDone);
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest("some message");
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
boost::asio::spawn(
ctx,
[this, &rpcEngineDone, fn = std::forward<decltype(fn)>(fn)](boost::asio::yield_context yield) {
EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
fn(yield);
rpcEngineDone.Call();
}
);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
rpcServerHandlerDone.Call();
EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request);
});
}
TEST_F(ng_RPCServerHandlerTest, JsonParseFailed)
{
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest("not a json");
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request);
});
}
TEST_F(ng_RPCServerHandlerTest, GotNotJsonObject)
{
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest("[]");
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
EXPECT_EQ(std::move(response).intoHttpResponse().result(), http::status::bad_request);
});
}
TEST_F(ng_RPCServerHandlerTest, HandleRequest_NoRangeFromBackend)
{
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest("{}");
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillOnce(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, notifyNotReady);
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "notReady");
});
}
TEST_F(ng_RPCServerHandlerTest, HandleRequest_ContextCreationFailed)
{
backend->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest("{}");
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, notifyBadSyntax);
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::bad_request);
EXPECT_EQ(httpResponse.body(), "Null method");
});
}
TEST_F(ng_RPCServerHandlerTest, HandleRequest_BuildResponseFailed)
{
backend->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest(R"json({"method":"some_method"})json");
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse)
.WillOnce(Return(rpc::Result{rpc::Status{rpc::ClioError::rpcUNKNOWN_OPTION}}));
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "unknownOption");
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1);
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::warnRPC_CLIO);
});
}
TEST_F(ng_RPCServerHandlerTest, HandleRequest_BuildResponseThrewAnException)
{
backend->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest(R"json({"method":"some_method"})json");
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse).WillOnce([](auto&&) -> rpc::Result {
throw std::runtime_error("some error");
});
EXPECT_CALL(*rpcEngine_, notifyInternalError);
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::internal_server_error);
});
}
TEST_F(ng_RPCServerHandlerTest, HandleRequest_Successful_HttpRequest)
{
backend->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest(R"json({"method":"some_method"})json");
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse)
.WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{{"some key", "some value"}}}}));
EXPECT_CALL(*rpcEngine_, notifyComplete);
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
EXPECT_EQ(jsonResponse.at("result").at("status").as_string(), "success");
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::warnRPC_CLIO);
});
}
TEST_F(ng_RPCServerHandlerTest, HandleRequest_OutdatedWarning)
{
backend->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest(R"json({"method":"some_method"})json");
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse)
.WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{{"some key", "some value"}}}}));
EXPECT_CALL(*rpcEngine_, notifyComplete);
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(61));
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
std::unordered_set<int64_t> warningCodes;
std::ranges::transform(
jsonResponse.at("warnings").as_array(),
std::inserter(warningCodes, warningCodes.end()),
[](auto const& w) { return w.as_object().at("id").as_int64(); }
);
EXPECT_EQ(warningCodes.size(), 2);
EXPECT_TRUE(warningCodes.contains(rpc::warnRPC_CLIO));
EXPECT_TRUE(warningCodes.contains(rpc::warnRPC_OUTDATED));
});
}
TEST_F(ng_RPCServerHandlerTest, HandleRequest_Successful_HttpRequest_Forwarded)
{
backend->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest(R"json({"method":"some_method"})json");
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse)
.WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{
{"result", boost::json::object{{"some key", "some value"}}}, {"forwarded", true}
}}}));
EXPECT_CALL(*rpcEngine_, notifyComplete);
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
EXPECT_EQ(jsonResponse.at("result").at("status").as_string(), "success");
EXPECT_EQ(jsonResponse.at("forwarded").as_bool(), true);
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::warnRPC_CLIO);
});
}
TEST_F(ng_RPCServerHandlerTest, HandleRequest_Successful_HttpRequest_HasError)
{
backend->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
auto const request = makeHttpRequest(R"json({"method":"some_method"})json");
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse)
.WillOnce(Return(rpc::Result{
rpc::ReturnType{boost::json::object{{"some key", "some value"}, {"error", "some error"}}}
}));
EXPECT_CALL(*rpcEngine_, notifyComplete);
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
fn(yield);
return true;
});
auto response = rpcServerHandler_(request, connectionMetadata_, nullptr, yield);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
auto const jsonResponse = boost::json::parse(httpResponse.body()).as_object();
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "some error");
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::warnRPC_CLIO);
});
}
struct ng_RPCServerHandlerWsTest : ng_RPCServerHandlerTest {
struct MockSubscriptionContext : web::SubscriptionContextInterface {
using web::SubscriptionContextInterface::SubscriptionContextInterface;
MOCK_METHOD(void, send, (std::shared_ptr<std::string>), (override));
MOCK_METHOD(void, onDisconnect, (web::SubscriptionContextInterface::OnDisconnectSlot const&), (override));
MOCK_METHOD(void, setApiSubversion, (uint32_t), (override));
MOCK_METHOD(uint32_t, apiSubversion, (), (const, override));
};
using StrictMockSubscriptionContext = testing::StrictMock<MockSubscriptionContext>;
std::shared_ptr<StrictMockSubscriptionContext> subscriptionContext_ =
std::make_shared<StrictMockSubscriptionContext>(tagFactory_);
};
TEST_F(ng_RPCServerHandlerWsTest, HandleRequest_Successful_WsRequest)
{
backend->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
Request::HttpHeaders headers;
auto const request = Request(R"json({"method":"some_method", "id": 1234, "api_version": 1})json", headers);
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse)
.WillOnce(Return(rpc::Result{rpc::ReturnType{boost::json::object{{"some key", "some value"}}}}));
EXPECT_CALL(*rpcEngine_, notifyComplete);
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
fn(yield);
return true;
});
auto const response = rpcServerHandler_(request, connectionMetadata_, subscriptionContext_, yield);
auto const jsonResponse = boost::json::parse(response.message()).as_object();
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
EXPECT_EQ(jsonResponse.at("status").as_string(), "success");
EXPECT_EQ(jsonResponse.at("type").as_string(), "response");
EXPECT_EQ(jsonResponse.at("id").as_int64(), 1234);
EXPECT_EQ(jsonResponse.at("api_version").as_int64(), 1);
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::warnRPC_CLIO);
});
}
TEST_F(ng_RPCServerHandlerWsTest, HandleRequest_Successful_WsRequest_HasError)
{
backend->setRange(0, 1);
runSpawn([&](boost::asio::yield_context yield) {
Request::HttpHeaders headers;
auto const request = Request(R"json({"method":"some_method", "id": 1234, "api_version": 1})json", headers);
EXPECT_CALL(*rpcEngine_, post).WillOnce([&](auto&& fn, auto&&) {
EXPECT_CALL(connectionMetadata_, wasUpgraded).WillRepeatedly(Return(not request.isHttp()));
EXPECT_CALL(*rpcEngine_, buildResponse)
.WillOnce(Return(rpc::Result{
rpc::ReturnType{boost::json::object{{"some key", "some value"}, {"error", "some error"}}}
}));
EXPECT_CALL(*rpcEngine_, notifyComplete);
EXPECT_CALL(*etl_, lastCloseAgeSeconds).WillOnce(Return(1));
fn(yield);
return true;
});
auto const response = rpcServerHandler_(request, connectionMetadata_, subscriptionContext_, yield);
auto const jsonResponse = boost::json::parse(response.message()).as_object();
EXPECT_EQ(jsonResponse.at("result").at("some key").as_string(), "some value");
EXPECT_EQ(jsonResponse.at("result").at("error").as_string(), "some error");
EXPECT_EQ(jsonResponse.at("type").as_string(), "response");
EXPECT_EQ(jsonResponse.at("id").as_int64(), 1234);
EXPECT_EQ(jsonResponse.at("api_version").as_int64(), 1);
ASSERT_EQ(jsonResponse.at("warnings").as_array().size(), 1) << jsonResponse;
EXPECT_EQ(jsonResponse.at("warnings").as_array().at(0).as_object().at("id").as_int64(), rpc::warnRPC_CLIO);
});
}

View File

@@ -26,8 +26,10 @@
#include <boost/beast/http/verb.hpp>
#include <gtest/gtest.h>
#include <iterator>
#include <optional>
#include <string>
#include <utility>
using namespace web::ng;
namespace http = boost::beast::http;
@@ -176,6 +178,35 @@ INSTANTIATE_TEST_SUITE_P(
tests::util::NameGenerator
);
struct RequestHttpHeadersTest : RequestTest {
http::field const headerName_ = http::field::user_agent;
std::string const headerValue_ = "clio";
};
TEST_F(RequestHttpHeadersTest, httpHeaders_HttpRequest)
{
auto httpRequest = http::request<http::string_body>{http::verb::get, "/", 11};
httpRequest.set(headerName_, headerValue_);
Request const request{std::move(httpRequest)};
auto const& headersFromRequest = request.httpHeaders();
ASSERT_EQ(headersFromRequest.count(headerName_), 1);
ASSERT_EQ(std::distance(headersFromRequest.cbegin(), headersFromRequest.cend()), 1);
EXPECT_EQ(headersFromRequest.at(headerName_), headerValue_);
}
TEST_F(RequestHttpHeadersTest, httpHeaders_WsRequest)
{
Request::HttpHeaders headers;
headers.set(headerName_, headerValue_);
Request const request{"websocket message", headers};
auto const& headersFromRequest = request.httpHeaders();
ASSERT_EQ(std::distance(headersFromRequest.cbegin(), headersFromRequest.cend()), 1);
ASSERT_EQ(headersFromRequest.count(headerName_), 1);
EXPECT_EQ(headersFromRequest.at(headerName_), headerValue_);
}
struct RequestHeaderValueTest : RequestTest {};
TEST_F(RequestHeaderValueTest, headerValue)

View File

@@ -50,7 +50,7 @@ TEST_F(ResponseDeathTest, asConstBufferWithHttpData)
{
Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
web::ng::Response const response{boost::beast::http::status::ok, "message", request};
EXPECT_DEATH(response.asConstBuffer(), "");
EXPECT_DEATH(response.asWsResponse(), "");
}
struct ResponseTest : testing::Test {
@@ -104,7 +104,7 @@ TEST_F(ResponseTest, asConstBuffer)
std::string const responseMessage = "response message";
web::ng::Response const response{responseStatus_, responseMessage, request};
auto const buffer = response.asConstBuffer();
auto const buffer = response.asWsResponse();
EXPECT_EQ(buffer.size(), responseMessage.size());
std::string const messageFromBuffer{static_cast<char const*>(buffer.data()), buffer.size()};
@@ -117,7 +117,7 @@ TEST_F(ResponseTest, asConstBufferJson)
boost::json::object const responseMessage{{"key", "value"}};
web::ng::Response const response{responseStatus_, responseMessage, request};
auto const buffer = response.asConstBuffer();
auto const buffer = response.asWsResponse();
EXPECT_EQ(buffer.size(), boost::json::serialize(responseMessage).size());
std::string const messageFromBuffer{static_cast<char const*>(buffer.data()), buffer.size()};

View File

@@ -25,7 +25,9 @@
#include "util/TestHttpClient.hpp"
#include "util/TestWebSocketClient.hpp"
#include "util/config/Config.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/ProcessingPolicy.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/Server.hpp"
@@ -48,7 +50,6 @@
#include <optional>
#include <ranges>
#include <string>
#include <utility>
using namespace web::ng;
@@ -164,20 +165,24 @@ struct ServerTest : SyncAsioContextTest {
std::string const headerName_ = "Some-header";
std::string const headerValue_ = "some value";
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
getHandler_;
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
postHandler_;
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
wsHandler_;
};
TEST_F(ServerTest, BadEndpoint)
{
boost::asio::ip::tcp::endpoint const endpoint{boost::asio::ip::address_v4::from_string("1.2.3.4"), 0};
impl::ConnectionHandler connectionHandler{impl::ConnectionHandler::ProcessingPolicy::Sequential, std::nullopt};
util::TagDecoratorFactory const tagDecoratorFactory{util::Config{boost::json::value{}}};
Server server{ctx, endpoint, std::nullopt, std::move(connectionHandler), tagDecoratorFactory};
Server server{
ctx, endpoint, std::nullopt, ProcessingPolicy::Sequential, std::nullopt, tagDecoratorFactory, std::nullopt
};
auto maybeError = server.run();
ASSERT_TRUE(maybeError.has_value());
EXPECT_THAT(*maybeError, testing::HasSubstr("Error creating TCP acceptor"));
@@ -251,7 +256,7 @@ TEST_P(ServerHttpTest, RequestResponse)
EXPECT_CALL(handler, Call)
.Times(3)
.WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&) {
.WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&, auto&&) {
EXPECT_TRUE(receivedRequest.isHttp());
EXPECT_EQ(receivedRequest.method(), GetParam().expectedMethod());
EXPECT_EQ(receivedRequest.message(), request.body());
@@ -317,7 +322,7 @@ TEST_F(ServerTest, WsRequestResponse)
EXPECT_CALL(wsHandler_, Call)
.Times(3)
.WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&) {
.WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&, auto&&) {
EXPECT_FALSE(receivedRequest.isHttp());
EXPECT_EQ(receivedRequest.method(), Request::Method::Websocket);
EXPECT_EQ(receivedRequest.message(), requestMessage_);

View File

@@ -0,0 +1,166 @@
//------------------------------------------------------------------------------
/*
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 "util/AsioContextTestFixture.hpp"
#include "util/Taggable.hpp"
#include "util/config/Config.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/SubscriptionContext.hpp"
#include "web/ng/impl/MockWsConnection.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/system/errc.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <cstddef>
#include <memory>
#include <optional>
#include <string>
using namespace web::ng;
struct ng_SubscriptionContextTests : SyncAsioContextTest {
util::TagDecoratorFactory tagFactory_{util::Config{}};
MockWsConnectionImpl connection_{"some ip", boost::beast::flat_buffer{}, tagFactory_};
testing::StrictMock<testing::MockFunction<bool(Error const&, Connection const&)>> errorHandler_;
SubscriptionContext
makeSubscriptionContext(boost::asio::yield_context yield, std::optional<size_t> maxSendQueueSize = std::nullopt)
{
return SubscriptionContext{tagFactory_, connection_, maxSendQueueSize, yield, errorHandler_.AsStdFunction()};
}
};
TEST_F(ng_SubscriptionContextTests, Send)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message = std::make_shared<std::string>("some message");
EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto, auto) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
return std::nullopt;
});
subscriptionContext.send(message);
subscriptionContext.disconnect(yield);
});
}
TEST_F(ng_SubscriptionContextTests, SendOrder)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message1 = std::make_shared<std::string>("message1");
auto const message2 = std::make_shared<std::string>("message2");
testing::Sequence sequence;
EXPECT_CALL(connection_, sendBuffer)
.InSequence(sequence)
.WillOnce([&message1](boost::asio::const_buffer buffer, auto, auto) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message1);
return std::nullopt;
});
EXPECT_CALL(connection_, sendBuffer)
.InSequence(sequence)
.WillOnce([&message2](boost::asio::const_buffer buffer, auto, auto) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message2);
return std::nullopt;
});
subscriptionContext.send(message1);
subscriptionContext.send(message2);
subscriptionContext.disconnect(yield);
});
}
TEST_F(ng_SubscriptionContextTests, SendFailed)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message = std::make_shared<std::string>("some message");
EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto, auto) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
return boost::system::errc::make_error_code(boost::system::errc::not_supported);
});
EXPECT_CALL(errorHandler_, Call).WillOnce(testing::Return(true));
EXPECT_CALL(connection_, close);
subscriptionContext.send(message);
subscriptionContext.disconnect(yield);
});
}
TEST_F(ng_SubscriptionContextTests, SendTooManySubscriptions)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield, 1);
auto const message = std::make_shared<std::string>("message1");
EXPECT_CALL(connection_, sendBuffer)
.WillOnce([&message](boost::asio::const_buffer buffer, boost::asio::yield_context innerYield, auto) {
boost::asio::post(innerYield); // simulate send is slow by switching to another coroutine
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
return std::nullopt;
});
EXPECT_CALL(connection_, close);
subscriptionContext.send(message);
subscriptionContext.send(message);
subscriptionContext.send(message);
subscriptionContext.disconnect(yield);
});
}
TEST_F(ng_SubscriptionContextTests, SendAfterDisconnect)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message = std::make_shared<std::string>("some message");
subscriptionContext.disconnect(yield);
subscriptionContext.send(message);
});
}
TEST_F(ng_SubscriptionContextTests, OnDisconnect)
{
testing::StrictMock<testing::MockFunction<void(web::SubscriptionContextInterface*)>> onDisconnect;
runSpawn([&](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
subscriptionContext.onDisconnect(onDisconnect.AsStdFunction());
EXPECT_CALL(onDisconnect, Call(&subscriptionContext));
subscriptionContext.disconnect(yield);
});
}
TEST_F(ng_SubscriptionContextTests, SetApiSubversion)
{
runSpawn([this](boost::asio::yield_context yield) {
auto subscriptionContext = makeSubscriptionContext(yield);
subscriptionContext.setApiSubversion(42);
EXPECT_EQ(subscriptionContext.apiSubversion(), 42);
});
}

View File

@@ -21,16 +21,21 @@
#include "util/Taggable.hpp"
#include "util/UnsupportedType.hpp"
#include "util/config/Config.hpp"
#include "web/SubscriptionContextInterface.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/MockConnection.hpp"
#include "web/ng/ProcessingPolicy.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/ConnectionHandler.hpp"
#include "web/ng/impl/MockHttpConnection.hpp"
#include "web/ng/impl/MockWsConnection.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http/error.hpp>
#include <boost/beast/http/message.hpp>
@@ -58,8 +63,8 @@ namespace http = boost::beast::http;
namespace websocket = boost::beast::websocket;
struct ConnectionHandlerTest : SyncAsioContextTest {
ConnectionHandlerTest(ConnectionHandler::ProcessingPolicy policy, std::optional<size_t> maxParallelConnections)
: connectionHandler_{policy, maxParallelConnections}
ConnectionHandlerTest(ProcessingPolicy policy, std::optional<size_t> maxParallelConnections)
: tagFactory_{util::Config{}}, connectionHandler_{policy, maxParallelConnections, tagFactory_, std::nullopt}
{
}
@@ -88,65 +93,71 @@ struct ConnectionHandlerTest : SyncAsioContextTest {
return Request{std::forward<Args>(args)...};
}
util::TagDecoratorFactory tagFactory_;
ConnectionHandler connectionHandler_;
util::TagDecoratorFactory tagDecoratorFactory_{util::Config(boost::json::object{{"log_tag_style", "uint"}})};
StrictMockConnectionPtr mockConnection_ =
std::make_unique<StrictMockConnection>("1.2.3.4", beast::flat_buffer{}, tagDecoratorFactory_);
StrictMockHttpConnectionPtr mockHttpConnection_ =
std::make_unique<StrictMockHttpConnection>("1.2.3.4", beast::flat_buffer{}, tagDecoratorFactory_);
StrictMockWsConnectionPtr mockWsConnection_ =
std::make_unique<StrictMockWsConnection>("1.2.3.4", beast::flat_buffer{}, tagDecoratorFactory_);
};
struct ConnectionHandlerSequentialProcessingTest : ConnectionHandlerTest {
ConnectionHandlerSequentialProcessingTest()
: ConnectionHandlerTest(ConnectionHandler::ProcessingPolicy::Sequential, std::nullopt)
ConnectionHandlerSequentialProcessingTest() : ConnectionHandlerTest(ProcessingPolicy::Sequential, std::nullopt)
{
}
};
TEST_F(ConnectionHandlerSequentialProcessingTest, ReceiveError)
{
EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(http::error::end_of_stream)));
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
EXPECT_CALL(*mockHttpConnection_, receive).WillOnce(Return(makeError(http::error::end_of_stream)));
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, ReceiveError_CloseConnection)
{
EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(boost::asio::error::timed_out)));
EXPECT_CALL(*mockConnection_, close);
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
EXPECT_CALL(*mockHttpConnection_, receive).WillOnce(Return(makeError(boost::asio::error::timed_out)));
EXPECT_CALL(*mockHttpConnection_, close);
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_NoHandler_Send)
{
EXPECT_CALL(*mockConnection_, receive)
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
EXPECT_CALL(*mockHttpConnection_, receive)
.WillOnce(Return(makeRequest("some_request", Request::HttpHeaders{})))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(*mockConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), "WebSocket is not supported by this server");
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadTarget_Send)
{
std::string const target = "/some/target";
std::string const requestMessage = "some message";
EXPECT_CALL(*mockConnection_, receive)
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
EXPECT_CALL(*mockHttpConnection_, receive)
.WillOnce(Return(makeRequest(http::request<http::string_body>{http::verb::get, target, 11, requestMessage})))
.WillOnce(Return(makeError(http::error::end_of_stream)));
EXPECT_CALL(*mockConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), "Bad target");
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::bad_request);
@@ -155,57 +166,126 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadTarget_Send)
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadMethod_Send)
{
EXPECT_CALL(*mockConnection_, receive)
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
EXPECT_CALL(*mockHttpConnection_, receive)
.WillOnce(Return(makeRequest(http::request<http::string_body>{http::verb::acl, "/", 11})))
.WillOnce(Return(makeError(http::error::end_of_stream)));
EXPECT_CALL(*mockConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), "Unsupported http method");
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
EXPECT_CALL(*mockConnection_, receive)
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
EXPECT_CALL(*mockWsConnection_, receive)
.WillOnce(Return(makeRequest(requestMessage, Request::HttpHeaders{})))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&) {
EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockWsConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
connectionHandler_.processConnection(std::move(mockWsConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send_Loop)
TEST_F(ConnectionHandlerSequentialProcessingTest, SendSubscriptionMessage)
{
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
std::string const subscriptionMessage = "subscription message";
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
EXPECT_CALL(*mockWsConnection_, receive)
.WillOnce(Return(makeRequest("", Request::HttpHeaders{})))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(wsHandlerMock, Call)
.WillOnce([&](Request const& request, auto&&, web::SubscriptionContextPtr subscriptionContext, auto&&) {
EXPECT_NE(subscriptionContext, nullptr);
subscriptionContext->send(std::make_shared<std::string>(subscriptionMessage));
return Response(http::status::ok, "", request);
});
EXPECT_CALL(*mockWsConnection_, send).WillOnce(Return(std::nullopt));
EXPECT_CALL(*mockWsConnection_, sendBuffer)
.WillOnce([&subscriptionMessage](boost::asio::const_buffer buffer, auto&&, auto&&) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), subscriptionMessage);
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockWsConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, SubscriptionContextIsDisconnectedAfterProcessingFinished)
{
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
testing::StrictMock<testing::MockFunction<void(web::SubscriptionContextInterface*)>> onDisconnectHook;
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
testing::Expectation const expectationReceiveCalled = EXPECT_CALL(*mockWsConnection_, receive)
.WillOnce(Return(makeRequest("", Request::HttpHeaders{})))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(wsHandlerMock, Call)
.WillOnce([&](Request const& request, auto&&, web::SubscriptionContextPtr subscriptionContext, auto&&) {
EXPECT_NE(subscriptionContext, nullptr);
subscriptionContext->onDisconnect(onDisconnectHook.AsStdFunction());
return Response(http::status::ok, "", request);
});
EXPECT_CALL(*mockWsConnection_, send).WillOnce(Return(std::nullopt));
EXPECT_CALL(onDisconnectHook, Call).After(expectationReceiveCalled);
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockWsConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, SubscriptionContextIsNullForHttpConnection)
{
std::string const target = "/some/target";
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
postHandlerMock;
connectionHandler_.onPost(target, postHandlerMock.AsStdFunction());
@@ -214,33 +294,76 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send_Loop)
auto const returnRequest =
Return(makeRequest(http::request<http::string_body>{http::verb::post, target, 11, requestMessage}));
EXPECT_CALL(*mockConnection_, receive)
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
EXPECT_CALL(*mockHttpConnection_, receive)
.WillOnce(returnRequest)
.WillOnce(Return(makeError(http::error::partial_message)));
EXPECT_CALL(postHandlerMock, Call)
.WillOnce([&](Request const& request, auto&&, web::SubscriptionContextPtr subscriptionContext, auto&&) {
EXPECT_EQ(subscriptionContext, nullptr);
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
EXPECT_CALL(*mockHttpConnection_, close);
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send_Loop)
{
std::string const target = "/some/target";
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
postHandlerMock;
connectionHandler_.onPost(target, postHandlerMock.AsStdFunction());
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
auto const returnRequest =
Return(makeRequest(http::request<http::string_body>{http::verb::post, target, 11, requestMessage}));
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
EXPECT_CALL(*mockHttpConnection_, receive)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(Return(makeError(http::error::partial_message)));
EXPECT_CALL(postHandlerMock, Call).Times(3).WillRepeatedly([&](Request const& request, auto&&, auto&&) {
EXPECT_CALL(postHandlerMock, Call).Times(3).WillRepeatedly([&](Request const& request, auto&&, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockConnection_, send).Times(3).WillRepeatedly([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
EXPECT_CALL(*mockHttpConnection_, send)
.Times(3)
.WillRepeatedly([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
EXPECT_CALL(*mockConnection_, close);
EXPECT_CALL(*mockHttpConnection_, close);
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_SendError)
{
std::string const target = "/some/target";
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
getHandlerMock;
std::string const requestMessage = "some message";
@@ -248,34 +371,38 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_SendError)
connectionHandler_.onGet(target, getHandlerMock.AsStdFunction());
EXPECT_CALL(*mockConnection_, receive)
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
EXPECT_CALL(*mockHttpConnection_, receive)
.WillOnce(Return(makeRequest(http::request<http::string_body>{http::verb::get, target, 11, requestMessage})));
EXPECT_CALL(getHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&) {
EXPECT_CALL(getHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return makeError(http::error::end_of_stream).error();
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
bool connectionClosed = false;
EXPECT_CALL(*mockConnection_, receive)
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
EXPECT_CALL(*mockWsConnection_, receive)
.Times(4)
.WillRepeatedly([&](auto&&, auto&&) -> std::expected<Request, Error> {
if (connectionClosed) {
@@ -284,13 +411,13 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
return makeRequest(requestMessage, Request::HttpHeaders{});
});
EXPECT_CALL(wsHandlerMock, Call).Times(3).WillRepeatedly([&](Request const& request, auto&&, auto&&) {
EXPECT_CALL(wsHandlerMock, Call).Times(3).WillRepeatedly([&](Request const& request, auto&&, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
size_t numCalls = 0;
EXPECT_CALL(*mockConnection_, send).Times(3).WillRepeatedly([&](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockWsConnection_, send).Times(3).WillRepeatedly([&](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
++numCalls;
@@ -300,10 +427,10 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
return std::nullopt;
});
EXPECT_CALL(*mockConnection_, close).WillOnce([&connectionClosed]() { connectionClosed = true; });
EXPECT_CALL(*mockWsConnection_, close).WillOnce([&connectionClosed]() { connectionClosed = true; });
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
connectionHandler_.processConnection(std::move(mockWsConnection_), yield);
});
}
@@ -312,7 +439,7 @@ struct ConnectionHandlerParallelProcessingTest : ConnectionHandlerTest {
ConnectionHandlerParallelProcessingTest()
: ConnectionHandlerTest(
ConnectionHandler::ProcessingPolicy::Parallel,
ProcessingPolicy::Parallel,
ConnectionHandlerParallelProcessingTest::maxParallelRequests
)
{
@@ -329,43 +456,48 @@ struct ConnectionHandlerParallelProcessingTest : ConnectionHandlerTest {
TEST_F(ConnectionHandlerParallelProcessingTest, ReceiveError)
{
EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(http::error::end_of_stream)));
EXPECT_CALL(*mockHttpConnection_, wasUpgraded).WillOnce(Return(false));
EXPECT_CALL(*mockHttpConnection_, receive).WillOnce(Return(makeError(http::error::end_of_stream)));
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
connectionHandler_.processConnection(std::move(mockHttpConnection_), yield);
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
EXPECT_CALL(*mockConnection_, receive)
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
EXPECT_CALL(*mockWsConnection_, receive)
.WillOnce(Return(makeRequest(requestMessage, Request::HttpHeaders{})))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&) {
EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, auto&&, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockWsConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
connectionHandler_.processConnection(std::move(mockWsConnection_), yield);
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
@@ -373,29 +505,34 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop)
std::string const responseMessage = "some response";
auto const returnRequest = [&](auto&&, auto&&) { return makeRequest(requestMessage, Request::HttpHeaders{}); };
EXPECT_CALL(*mockConnection_, receive)
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
EXPECT_CALL(*mockWsConnection_, receive)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(wsHandlerMock, Call).Times(2).WillRepeatedly([&](Request const& request, auto&&, auto&&) {
EXPECT_CALL(wsHandlerMock, Call).Times(2).WillRepeatedly([&](Request const& request, auto&&, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockConnection_, send).Times(2).WillRepeatedly([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
EXPECT_CALL(*mockWsConnection_, send)
.Times(2)
.WillRepeatedly([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
connectionHandler_.processConnection(std::move(mockWsConnection_), yield);
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooManyRequest)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
testing::StrictMock<testing::MockFunction<
Response(Request const&, ConnectionMetadata const&, web::SubscriptionContextPtr, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
@@ -404,7 +541,9 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooMany
auto const returnRequest = [&](auto&&, auto&&) { return makeRequest(requestMessage, Request::HttpHeaders{}); };
testing::Sequence const sequence;
EXPECT_CALL(*mockConnection_, receive)
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
EXPECT_CALL(*mockWsConnection_, receive)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
@@ -414,14 +553,14 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooMany
EXPECT_CALL(wsHandlerMock, Call)
.Times(3)
.WillRepeatedly([&](Request const& request, auto&&, boost::asio::yield_context yield) {
.WillRepeatedly([&](Request const& request, auto&&, auto&&, boost::asio::yield_context yield) {
EXPECT_EQ(request.message(), requestMessage);
asyncSleep(yield, std::chrono::milliseconds{3});
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(
*mockConnection_,
*mockWsConnection_,
send(
testing::ResultOf([](Response response) { return response.message(); }, responseMessage),
testing::_,
@@ -432,7 +571,7 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooMany
.WillRepeatedly(Return(std::nullopt));
EXPECT_CALL(
*mockConnection_,
*mockWsConnection_,
send(
testing::ResultOf(
[](Response response) { return response.message(); }, "Too many requests for one connection"
@@ -445,6 +584,6 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooMany
.WillRepeatedly(Return(std::nullopt));
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
connectionHandler_.processConnection(std::move(mockWsConnection_), yield);
});
}

View File

@@ -0,0 +1,358 @@
//------------------------------------------------------------------------------
/*
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 "rpc/Errors.hpp"
#include "util/LoggerFixtures.hpp"
#include "util/NameGenerator.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/impl/ErrorHandling.hpp"
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.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 <optional>
#include <string>
#include <utility>
#include <variant>
using namespace web::ng::impl;
using namespace web::ng;
namespace http = boost::beast::http;
struct ng_ErrorHandlingTests : NoLoggerFixture {
static Request
makeRequest(bool isHttp, std::optional<std::string> body = std::nullopt)
{
if (isHttp)
return Request{http::request<http::string_body>{http::verb::post, "/", 11, body.value_or("")}};
return Request{body.value_or(""), Request::HttpHeaders{}};
}
};
struct ng_ErrorHandlingMakeErrorTestBundle {
std::string testName;
bool isHttp;
rpc::Status status;
std::string expectedMessage;
boost::beast::http::status expectedStatus;
};
struct ng_ErrorHandlingMakeErrorTest : ng_ErrorHandlingTests,
testing::WithParamInterface<ng_ErrorHandlingMakeErrorTestBundle> {};
TEST_P(ng_ErrorHandlingMakeErrorTest, MakeError)
{
auto const request = makeRequest(GetParam().isHttp);
ErrorHelper errorHelper{request};
auto response = errorHelper.makeError(GetParam().status);
EXPECT_EQ(response.message(), GetParam().expectedMessage);
if (GetParam().isHttp) {
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), GetParam().expectedStatus);
std::string expectedContentType = "text/html";
if (std::holds_alternative<rpc::RippledError>(GetParam().status.code))
expectedContentType = "application/json";
EXPECT_EQ(httpResponse.at(http::field::content_type), expectedContentType);
}
}
INSTANTIATE_TEST_CASE_P(
ng_ErrorHandlingMakeErrorTestGroup,
ng_ErrorHandlingMakeErrorTest,
testing::ValuesIn({
ng_ErrorHandlingMakeErrorTestBundle{
"WsRequest",
false,
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})",
boost::beast::http::status::ok
},
ng_ErrorHandlingMakeErrorTestBundle{
"HttpRequest_InvalidApiVersion",
true,
rpc::Status{rpc::ClioError::rpcINVALID_API_VERSION},
"invalid_API_version",
boost::beast::http::status::bad_request
},
ng_ErrorHandlingMakeErrorTestBundle{
"HttpRequest_CommandIsMissing",
true,
rpc::Status{rpc::ClioError::rpcCOMMAND_IS_MISSING},
"Null method",
boost::beast::http::status::bad_request
},
ng_ErrorHandlingMakeErrorTestBundle{
"HttpRequest_CommandIsEmpty",
true,
rpc::Status{rpc::ClioError::rpcCOMMAND_IS_EMPTY},
"method is empty",
boost::beast::http::status::bad_request
},
ng_ErrorHandlingMakeErrorTestBundle{
"HttpRequest_CommandNotString",
true,
rpc::Status{rpc::ClioError::rpcCOMMAND_NOT_STRING},
"method is not string",
boost::beast::http::status::bad_request
},
ng_ErrorHandlingMakeErrorTestBundle{
"HttpRequest_ParamsUnparseable",
true,
rpc::Status{rpc::ClioError::rpcPARAMS_UNPARSEABLE},
"params unparseable",
boost::beast::http::status::bad_request
},
ng_ErrorHandlingMakeErrorTestBundle{
"HttpRequest_RippledError",
true,
rpc::Status{rpc::RippledError::rpcTOO_BUSY},
R"({"result":{"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"}})",
boost::beast::http::status::bad_request
},
}),
tests::util::NameGenerator
);
struct ng_ErrorHandlingMakeInternalErrorTestBundle {
std::string testName;
bool isHttp;
std::optional<std::string> request;
boost::json::object expectedResult;
};
struct ng_ErrorHandlingMakeInternalErrorTest
: ng_ErrorHandlingTests,
testing::WithParamInterface<ng_ErrorHandlingMakeInternalErrorTestBundle> {};
TEST_P(ng_ErrorHandlingMakeInternalErrorTest, ComposeError)
{
auto const request = makeRequest(GetParam().isHttp, GetParam().request);
std::optional<boost::json::object> const requestJson = GetParam().request.has_value()
? std::make_optional(boost::json::parse(*GetParam().request).as_object())
: std::nullopt;
ErrorHelper errorHelper{request, requestJson};
auto response = errorHelper.makeInternalError();
EXPECT_EQ(response.message(), boost::json::serialize(GetParam().expectedResult));
if (GetParam().isHttp) {
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::internal_server_error);
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
}
}
INSTANTIATE_TEST_CASE_P(
ng_ErrorHandlingComposeErrorTestGroup,
ng_ErrorHandlingMakeInternalErrorTest,
testing::ValuesIn(
{ng_ErrorHandlingMakeInternalErrorTestBundle{
"NoRequest_WebsocketConnection",
false,
std::nullopt,
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"}}
},
ng_ErrorHandlingMakeInternalErrorTestBundle{
"NoRequest_HttpConnection",
true,
std::nullopt,
{{"result",
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"}}}}
},
ng_ErrorHandlingMakeInternalErrorTestBundle{
"Request_WebsocketConnection",
false,
std::string{R"({"id": 1, "api_version": 2})"},
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"},
{"id", 1},
{"api_version", 2},
{"request", {{"id", 1}, {"api_version", 2}}}}
},
ng_ErrorHandlingMakeInternalErrorTestBundle{
"Request_WebsocketConnection_NoId",
false,
std::string{R"({"api_version": 2})"},
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"},
{"api_version", 2},
{"request", {{"api_version", 2}}}}
},
ng_ErrorHandlingMakeInternalErrorTestBundle{
"Request_HttpConnection",
true,
std::string{R"({"id": 1, "api_version": 2})"},
{{"result",
{{"error", "internal"},
{"error_code", 73},
{"error_message", "Internal error."},
{"status", "error"},
{"type", "response"},
{"id", 1},
{"request", {{"id", 1}, {"api_version", 2}}}}}}
}}
),
tests::util::NameGenerator
);
TEST_F(ng_ErrorHandlingTests, MakeNotReadyError)
{
auto const request = makeRequest(true);
auto response = ErrorHelper{request}.makeNotReadyError();
EXPECT_EQ(
response.message(),
std::string{
R"({"result":{"error":"notReady","error_code":13,"error_message":"Not ready to handle this request.","status":"error","type":"response"}})"
}
);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::ok);
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
}
TEST_F(ng_ErrorHandlingTests, MakeTooBusyError_WebsocketRequest)
{
auto const request = makeRequest(false);
auto response = ErrorHelper{request}.makeTooBusyError();
EXPECT_EQ(
response.message(),
std::string{
R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})"
}
);
}
TEST_F(ng_ErrorHandlingTests, sendTooBusyError_HttpConnection)
{
auto const request = makeRequest(true);
auto response = ErrorHelper{request}.makeTooBusyError();
EXPECT_EQ(
response.message(),
std::string{
R"({"error":"tooBusy","error_code":9,"error_message":"The server is too busy to help you now.","status":"error","type":"response"})"
}
);
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::service_unavailable);
EXPECT_EQ(httpResponse.at(http::field::content_type), "application/json");
}
TEST_F(ng_ErrorHandlingTests, makeJsonParsingError_WebsocketConnection)
{
auto const request = makeRequest(false);
auto response = ErrorHelper{request}.makeJsonParsingError();
EXPECT_EQ(
response.message(),
std::string{
R"({"error":"badSyntax","error_code":1,"error_message":"Syntax error.","status":"error","type":"response"})"
}
);
}
TEST_F(ng_ErrorHandlingTests, makeJsonParsingError_HttpConnection)
{
auto const request = makeRequest(true);
auto response = ErrorHelper{request}.makeJsonParsingError();
EXPECT_EQ(response.message(), std::string{"Unable to parse JSON from the request"});
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), boost::beast::http::status::bad_request);
EXPECT_EQ(httpResponse.at(http::field::content_type), "text/html");
}
struct ng_ErrorHandlingComposeErrorTestBundle {
std::string testName;
bool isHttp;
std::optional<boost::json::object> request;
std::string expectedMessage;
};
struct ng_ErrorHandlingComposeErrorTest : ng_ErrorHandlingTests,
testing::WithParamInterface<ng_ErrorHandlingComposeErrorTestBundle> {};
TEST_P(ng_ErrorHandlingComposeErrorTest, ComposeError)
{
auto const request = makeRequest(GetParam().isHttp);
ErrorHelper errorHelper{request, GetParam().request};
auto const response = errorHelper.composeError(rpc::Status{rpc::RippledError::rpcINTERNAL});
EXPECT_EQ(boost::json::serialize(response), GetParam().expectedMessage);
}
INSTANTIATE_TEST_CASE_P(
ng_ErrorHandlingComposeErrorTestGroup,
ng_ErrorHandlingComposeErrorTest,
testing::ValuesIn(
{ng_ErrorHandlingComposeErrorTestBundle{
"NoRequest_WebsocketConnection",
false,
std::nullopt,
R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})"
},
ng_ErrorHandlingComposeErrorTestBundle{
"NoRequest_HttpConnection",
true,
std::nullopt,
R"({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"}})"
},
ng_ErrorHandlingComposeErrorTestBundle{
"Request_WebsocketConnection",
false,
boost::json::object{{"id", 1}, {"api_version", 2}},
R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"api_version":2,"request":{"id":1,"api_version":2}})",
},
ng_ErrorHandlingComposeErrorTestBundle{
"Request_WebsocketConnection_NoId",
false,
boost::json::object{{"api_version", 2}},
R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","api_version":2,"request":{"api_version":2}})",
},
ng_ErrorHandlingComposeErrorTestBundle{
"Request_HttpConnection",
true,
boost::json::object{{"id", 1}, {"api_version", 2}},
R"({"result":{"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":1,"request":{"id":1,"api_version":2}}})"
}}
),
tests::util::NameGenerator
);

View File

@@ -27,6 +27,7 @@
#include "web/ng/Response.hpp"
#include "web/ng/impl/HttpConnection.hpp"
#include <boost/asio/error.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
@@ -37,6 +38,7 @@
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/json/object.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
@@ -94,7 +96,6 @@ TEST_F(HttpConnectionTests, Receive)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
EXPECT_TRUE(connection.ip() == "127.0.0.1" or connection.ip() == "::1") << connection.ip();
auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{100});
ASSERT_TRUE(expectedRequest.has_value()) << expectedRequest.error().message();
@@ -293,3 +294,39 @@ TEST_F(HttpConnectionTests, Upgrade)
[&]() { ASSERT_TRUE(expectedWsConnection.has_value()) << expectedWsConnection.error().message(); }();
});
}
TEST_F(HttpConnectionTests, Ip)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) mutable {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
});
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
EXPECT_TRUE(connection.ip() == "127.0.0.1" or connection.ip() == "::1") << connection.ip();
});
}
TEST_F(HttpConnectionTests, isAdminSetAdmin)
{
testing::StrictMock<testing::MockFunction<bool()>> adminSetter;
EXPECT_CALL(adminSetter, Call).WillOnce(testing::Return(true));
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) mutable {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
});
runSpawn([&](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
EXPECT_FALSE(connection.isAdmin());
connection.setIsAdmin(adminSetter.AsStdFunction());
EXPECT_TRUE(connection.isAdmin());
// Setter shouldn't not be called here because isAdmin is already set
connection.setIsAdmin(adminSetter.AsStdFunction());
EXPECT_TRUE(connection.isAdmin());
});
}

View File

@@ -23,7 +23,10 @@
#include "web/ng/impl/ServerSslContext.hpp"
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/value.hpp>
#include <fmt/compile.h>
#include <fmt/core.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <test_data/SslCert.hpp>