refactor: Coroutine based webserver (#1699)

Code of new coroutine-based web server. The new server is not connected
to Clio and not ready to use yet.
For #919.
This commit is contained in:
Sergey Kuznetsov
2024-10-24 16:50:26 +01:00
committed by GitHub
parent cf081e7e25
commit cffda52ba6
58 changed files with 5581 additions and 488 deletions

View File

@@ -1,8 +1,15 @@
add_library(clio_testing_common)
target_sources(
clio_testing_common PRIVATE util/StringUtils.cpp util/TestHttpServer.cpp util/TestWsServer.cpp util/TestObject.cpp
util/AssignRandomPort.cpp util/WithTimeout.cpp
clio_testing_common
PRIVATE util/AssignRandomPort.cpp
util/CallWithTimeout.cpp
util/StringUtils.cpp
util/TestHttpClient.cpp
util/TestHttpServer.cpp
util/TestObject.cpp
util/TestWebSocketClient.cpp
util/TestWsServer.cpp
)
include(deps/gtest)

View File

@@ -17,7 +17,7 @@
*/
//==============================================================================
#include "util/WithTimeout.hpp"
#include "util/CallWithTimeout.hpp"
#include <gtest/gtest.h>
@@ -30,7 +30,7 @@
namespace tests::common::util {
void
withTimeout(std::chrono::steady_clock::duration timeout, std::function<void()> function)
callWithTimeout(std::chrono::steady_clock::duration timeout, std::function<void()> function)
{
std::promise<void> promise;
auto future = promise.get_future();

View File

@@ -31,6 +31,6 @@ namespace tests::common::util {
* @param function The function to run
*/
void
withTimeout(std::chrono::steady_clock::duration timeout, std::function<void()> function);
callWithTimeout(std::chrono::steady_clock::duration timeout, std::function<void()> function);
} // namespace tests::common::util

View File

@@ -0,0 +1,250 @@
//------------------------------------------------------------------------------
/*
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/TestHttpClient.hpp"
#include "util/Assert.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/error.hpp>
#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>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/beast/ssl/ssl_stream.hpp>
#include <boost/beast/version.hpp>
#include <boost/beast/websocket/rfc6455.hpp>
#include <boost/beast/websocket/stream.hpp>
#include <boost/beast/websocket/stream_base.hpp>
#include <openssl/err.h>
#include <openssl/tls1.h>
#include <chrono>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace http = boost::beast::http;
namespace net = boost::asio;
namespace ssl = boost::asio::ssl;
using tcp = boost::asio::ip::tcp;
namespace {
std::string
syncRequest(
std::string const& host,
std::string const& port,
std::string const& body,
std::vector<WebHeader> additionalHeaders,
http::verb method,
std::string target = "/"
)
{
boost::asio::io_context ioc;
net::ip::tcp::resolver resolver(ioc);
boost::beast::tcp_stream stream(ioc);
auto const results = resolver.resolve(host, port);
stream.connect(results);
http::request<http::string_body> req{method, "/", 10};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
for (auto& header : additionalHeaders) {
req.set(header.name, header.value);
}
req.target(target);
req.body() = std::string(body);
req.prepare_payload();
http::write(stream, req);
boost::beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(stream, buffer, res);
boost::beast::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
return res.body();
}
} // namespace
WebHeader::WebHeader(http::field name, std::string value) : name(name), value(std::move(value))
{
}
std::string
HttpSyncClient::post(
std::string const& host,
std::string const& port,
std::string const& body,
std::vector<WebHeader> additionalHeaders
)
{
return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::post);
}
std::string
HttpSyncClient::get(
std::string const& host,
std::string const& port,
std::string const& body,
std::string const& target,
std::vector<WebHeader> additionalHeaders
)
{
return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::get, target);
}
bool
HttpsSyncClient::verify_certificate(bool /* preverified */, boost::asio::ssl::verify_context& /* ctx */)
{
return true;
}
std::string
HttpsSyncClient::syncPost(std::string const& host, std::string const& port, std::string const& body)
{
net::io_context ioc;
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
ctx.set_default_verify_paths();
ctx.set_verify_mode(ssl::verify_none);
tcp::resolver resolver(ioc);
boost::beast::ssl_stream<boost::beast::tcp_stream> stream(ioc, ctx);
// We can't fix this so have to ignore
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wold-style-cast"
if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str()))
#pragma GCC diagnostic pop
{
boost::beast::error_code const ec{static_cast<int>(::ERR_get_error()), net::error::get_ssl_category()};
throw boost::beast::system_error{ec};
}
auto const results = resolver.resolve(host, port);
boost::beast::get_lowest_layer(stream).connect(results);
stream.handshake(ssl::stream_base::client);
http::request<http::string_body> req{http::verb::post, "/", 10};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req.body() = std::string(body);
req.prepare_payload();
http::write(stream, req);
boost::beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(stream, buffer, res);
boost::beast::error_code ec;
stream.shutdown(ec);
return res.body();
}
HttpAsyncClient::HttpAsyncClient(boost::asio::io_context& ioContext) : stream_{ioContext}
{
}
std::optional<boost::system::error_code>
HttpAsyncClient::connect(
std::string_view host,
std::string_view port,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout
)
{
boost::system::error_code error;
boost::asio::ip::tcp::resolver resolver{stream_.get_executor()};
auto const resolverResults = resolver.resolve(host, port, error);
if (error)
return error;
ASSERT(resolverResults.size() > 0, "No results from resolver");
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
stream_.async_connect(resolverResults.begin()->endpoint(), yield[error]);
if (error)
return error;
return std::nullopt;
}
std::optional<boost::system::error_code>
HttpAsyncClient::send(
boost::beast::http::request<boost::beast::http::string_body> request,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout
)
{
request.prepare_payload();
boost::system::error_code error;
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
http::async_write(stream_, request, yield[error]);
if (error)
return error;
return std::nullopt;
}
std::expected<boost::beast::http::response<boost::beast::http::string_body>, boost::system::error_code>
HttpAsyncClient::receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::system::error_code error;
http::response<http::string_body> response;
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
http::async_read(stream_, buffer_, response, yield[error]);
if (error)
return std::unexpected{error};
return response;
}
void
HttpAsyncClient::gracefulShutdown()
{
boost::system::error_code error;
stream_.socket().shutdown(tcp::socket::shutdown_both, error);
}
void
HttpAsyncClient::disconnect()
{
boost::system::error_code error;
stream_.socket().close(error);
}

View File

@@ -0,0 +1,99 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/verify_context.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <chrono>
#include <expected>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
struct WebHeader {
WebHeader(boost::beast::http::field name, std::string value);
boost::beast::http::field name;
std::string value;
};
struct HttpSyncClient {
static std::string
post(
std::string const& host,
std::string const& port,
std::string const& body,
std::vector<WebHeader> additionalHeaders = {}
);
static std::string
get(std::string const& host,
std::string const& port,
std::string const& body,
std::string const& target,
std::vector<WebHeader> additionalHeaders = {});
};
struct HttpsSyncClient {
static bool
verify_certificate(bool /* preverified */, boost::asio::ssl::verify_context& /* ctx */);
static std::string
syncPost(std::string const& host, std::string const& port, std::string const& body);
};
class HttpAsyncClient {
boost::beast::tcp_stream stream_;
boost::beast::flat_buffer buffer_;
public:
HttpAsyncClient(boost::asio::io_context& ioContext);
std::optional<boost::system::error_code>
connect(
std::string_view host,
std::string_view port,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout
);
std::optional<boost::system::error_code>
send(
boost::beast::http::request<boost::beast::http::string_body> request,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout
);
std::expected<boost::beast::http::response<boost::beast::http::string_body>, boost::system::error_code>
receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout);
void
gracefulShutdown();
void
disconnect();
};

View File

@@ -19,6 +19,8 @@
#include "util/TestHttpServer.hpp"
#include "util/Assert.hpp"
#include <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/address.hpp>
@@ -36,6 +38,7 @@
#include <gtest/gtest.h>
#include <chrono>
#include <expected>
#include <string>
#include <utility>
@@ -107,13 +110,27 @@ doSession(
TestHttpServer::TestHttpServer(boost::asio::io_context& context, std::string host) : acceptor_(context)
{
boost::asio::ip::tcp::endpoint const endpoint(boost::asio::ip::make_address(host), 0);
boost::asio::ip::tcp::resolver resolver{context};
auto const results = resolver.resolve(host, "0");
ASSERT(!results.empty(), "Failed to resolve host");
boost::asio::ip::tcp::endpoint const& endpoint = results.begin()->endpoint();
acceptor_.open(endpoint.protocol());
acceptor_.set_option(asio::socket_base::reuse_address(true));
acceptor_.bind(endpoint);
acceptor_.listen(asio::socket_base::max_listen_connections);
}
std::expected<boost::asio::ip::tcp::socket, boost::system::error_code>
TestHttpServer::accept(boost::asio::yield_context yield)
{
boost::beast::error_code errorCode;
tcp::socket socket(this->acceptor_.get_executor());
acceptor_.async_accept(socket, yield[errorCode]);
if (errorCode)
return std::unexpected{errorCode};
return socket;
}
void
TestHttpServer::handleRequest(TestHttpServer::RequestHandler handler, bool const allowToFail)
{

View File

@@ -21,9 +21,11 @@
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <expected>
#include <functional>
#include <optional>
#include <string>
@@ -44,6 +46,15 @@ public:
*/
TestHttpServer(boost::asio::io_context& context, std::string host);
/**
* @brief Accept a new connection
*
* @param yield boost::asio::yield_context to use for networking
* @return Either a socket with the new connection or an error code
*/
std::expected<boost::asio::ip::tcp::socket, boost::system::error_code>
accept(boost::asio::yield_context yield);
/**
* @brief Start the server
*

View File

@@ -1,270 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <boost/asio/buffer.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/error.hpp>
#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>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/beast/ssl/ssl_stream.hpp>
#include <boost/beast/version.hpp>
#include <boost/beast/websocket/rfc6455.hpp>
#include <boost/beast/websocket/stream.hpp>
#include <boost/beast/websocket/stream_base.hpp>
#include <openssl/err.h>
#include <openssl/tls1.h>
#include <optional>
#include <string>
#include <utility>
#include <vector>
namespace http = boost::beast::http;
namespace net = boost::asio;
namespace ssl = boost::asio::ssl;
using tcp = boost::asio::ip::tcp;
struct WebHeader {
WebHeader(http::field name, std::string value) : name(name), value(std::move(value))
{
}
http::field name;
std::string value;
};
struct HttpSyncClient {
static std::string
syncPost(
std::string const& host,
std::string const& port,
std::string const& body,
std::vector<WebHeader> additionalHeaders = {}
)
{
return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::post);
}
static std::string
syncGet(
std::string const& host,
std::string const& port,
std::string const& body,
std::string const& target,
std::vector<WebHeader> additionalHeaders = {}
)
{
return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::get, target);
}
private:
static std::string
syncRequest(
std::string const& host,
std::string const& port,
std::string const& body,
std::vector<WebHeader> additionalHeaders,
http::verb method,
std::string target = "/"
)
{
boost::asio::io_context ioc;
net::ip::tcp::resolver resolver(ioc);
boost::beast::tcp_stream stream(ioc);
auto const results = resolver.resolve(host, port);
stream.connect(results);
http::request<http::string_body> req{method, "/", 10};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
for (auto& header : additionalHeaders) {
req.set(header.name, header.value);
}
req.target(target);
req.body() = std::string(body);
req.prepare_payload();
http::write(stream, req);
boost::beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(stream, buffer, res);
boost::beast::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
return res.body();
}
};
class WebSocketSyncClient {
net::io_context ioc_;
tcp::resolver resolver_{ioc_};
boost::beast::websocket::stream<tcp::socket> ws_{ioc_};
public:
void
connect(std::string const& host, std::string const& port, std::vector<WebHeader> additionalHeaders = {})
{
auto const results = resolver_.resolve(host, port);
auto const ep = net::connect(ws_.next_layer(), results);
// Update the host_ string. This will provide the value of the
// Host HTTP header during the WebSocket handshake.
// See https://tools.ietf.org/html/rfc7230#section-5.4
auto const hostPort = host + ':' + std::to_string(ep.port());
ws_.set_option(boost::beast::websocket::stream_base::decorator([additionalHeaders = std::move(additionalHeaders
)](boost::beast::websocket::request_type& req) {
req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
for (auto const& header : additionalHeaders) {
req.set(header.name, header.value);
}
}));
ws_.handshake(hostPort, "/");
}
void
disconnect()
{
ws_.close(boost::beast::websocket::close_code::normal);
}
std::string
syncPost(std::string const& body)
{
boost::beast::flat_buffer buffer;
ws_.write(net::buffer(std::string(body)));
ws_.read(buffer);
return boost::beast::buffers_to_string(buffer.data());
}
};
struct HttpsSyncClient {
static bool
verify_certificate(bool /* preverified */, boost::asio::ssl::verify_context& /* ctx */)
{
return true;
}
static std::string
syncPost(std::string const& host, std::string const& port, std::string const& body)
{
net::io_context ioc;
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
ctx.set_default_verify_paths();
ctx.set_verify_mode(ssl::verify_none);
tcp::resolver resolver(ioc);
boost::beast::ssl_stream<boost::beast::tcp_stream> stream(ioc, ctx);
// We can't fix this so have to ignore
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wold-style-cast"
if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str()))
#pragma GCC diagnostic pop
{
boost::beast::error_code const ec{static_cast<int>(::ERR_get_error()), net::error::get_ssl_category()};
throw boost::beast::system_error{ec};
}
auto const results = resolver.resolve(host, port);
boost::beast::get_lowest_layer(stream).connect(results);
stream.handshake(ssl::stream_base::client);
http::request<http::string_body> req{http::verb::post, "/", 10};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req.body() = std::string(body);
req.prepare_payload();
http::write(stream, req);
boost::beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(stream, buffer, res);
boost::beast::error_code ec;
stream.shutdown(ec);
return res.body();
}
};
class WebServerSslSyncClient {
net::io_context ioc_;
std::optional<boost::beast::websocket::stream<boost::beast::ssl_stream<tcp::socket>>> ws_;
public:
void
connect(std::string const& host, std::string const& port)
{
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
ctx.set_default_verify_paths();
ctx.set_verify_mode(ssl::verify_none);
tcp::resolver resolver{ioc_};
ws_.emplace(ioc_, ctx);
auto const results = resolver.resolve(host, port);
net::connect(ws_->next_layer().next_layer(), results.begin(), results.end());
ws_->next_layer().handshake(ssl::stream_base::client);
ws_->set_option(boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::request_type& req) {
req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
}));
ws_->handshake(host, "/");
}
void
disconnect()
{
ws_->close(boost::beast::websocket::close_code::normal);
}
std::string
syncPost(std::string const& body)
{
boost::beast::flat_buffer buffer;
ws_->write(net::buffer(std::string(body)));
ws_->read(buffer);
return boost::beast::buffers_to_string(buffer.data());
}
};

View File

@@ -0,0 +1,225 @@
//------------------------------------------------------------------------------
/*
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/TestWebSocketClient.hpp"
#include "util/Assert.hpp"
#include "util/TestHttpClient.hpp"
#include "util/WithTimeout.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/error.hpp>
#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>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/beast/ssl/ssl_stream.hpp>
#include <boost/beast/version.hpp>
#include <boost/beast/websocket/rfc6455.hpp>
#include <boost/beast/websocket/stream.hpp>
#include <boost/beast/websocket/stream_base.hpp>
#include <fmt/core.h>
#include <openssl/err.h>
#include <openssl/tls1.h>
#include <chrono>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace http = boost::beast::http;
namespace net = boost::asio;
namespace ssl = boost::asio::ssl;
using tcp = boost::asio::ip::tcp;
void
WebSocketSyncClient::connect(std::string const& host, std::string const& port, std::vector<WebHeader> additionalHeaders)
{
auto const results = resolver_.resolve(host, port);
auto const ep = net::connect(ws_.next_layer(), results);
// Update the host_ string. This will provide the value of the
// Host HTTP header during the WebSocket handshake.
// See https://tools.ietf.org/html/rfc7230#section-5.4
auto const hostPort = host + ':' + std::to_string(ep.port());
ws_.set_option(boost::beast::websocket::stream_base::decorator([additionalHeaders = std::move(additionalHeaders
)](boost::beast::websocket::request_type& req) {
req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
for (auto const& header : additionalHeaders) {
req.set(header.name, header.value);
}
}));
ws_.handshake(hostPort, "/");
}
void
WebSocketSyncClient::disconnect()
{
ws_.close(boost::beast::websocket::close_code::normal);
}
std::string
WebSocketSyncClient::syncPost(std::string const& body)
{
boost::beast::flat_buffer buffer;
ws_.write(net::buffer(std::string(body)));
ws_.read(buffer);
return boost::beast::buffers_to_string(buffer.data());
}
void
WebServerSslSyncClient::connect(std::string const& host, std::string const& port)
{
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
ctx.set_default_verify_paths();
ctx.set_verify_mode(ssl::verify_none);
tcp::resolver resolver{ioc_};
ws_.emplace(ioc_, ctx);
auto const results = resolver.resolve(host, port);
net::connect(ws_->next_layer().next_layer(), results.begin(), results.end());
ws_->next_layer().handshake(ssl::stream_base::client);
ws_->set_option(boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::request_type& req) {
req.set(http::field::user_agent, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
}));
ws_->handshake(host, "/");
}
void
WebServerSslSyncClient::disconnect()
{
ws_->close(boost::beast::websocket::close_code::normal);
}
std::string
WebServerSslSyncClient::syncPost(std::string const& body)
{
boost::beast::flat_buffer buffer;
ws_->write(net::buffer(std::string(body)));
ws_->read(buffer);
return boost::beast::buffers_to_string(buffer.data());
}
WebSocketAsyncClient::WebSocketAsyncClient(boost::asio::io_context& ioContext) : stream_{ioContext}
{
}
std::optional<boost::system::error_code>
WebSocketAsyncClient::connect(
std::string const& host,
std::string const& port,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout,
std::vector<WebHeader> additionalHeaders
)
{
auto const results = boost::asio::ip::tcp::resolver{yield.get_executor()}.resolve(host, port);
ASSERT(not results.empty(), "Could not resolve {}:{}", host, port);
boost::system::error_code error;
boost::beast::get_lowest_layer(stream_).expires_after(timeout);
stream_.next_layer().async_connect(results, yield[error]);
if (error)
return error;
boost::beast::websocket::stream_base::timeout wsTimeout{};
stream_.get_option(wsTimeout);
wsTimeout.handshake_timeout = timeout;
stream_.set_option(wsTimeout);
boost::beast::get_lowest_layer(stream_).expires_never();
stream_.set_option(boost::beast::websocket::stream_base::decorator([additionalHeaders = std::move(additionalHeaders
)](boost::beast::websocket::request_type& req) {
for (auto const& header : additionalHeaders) {
req.set(header.name, header.value);
}
}));
stream_.async_handshake(fmt::format("{}:{}", host, port), "/", yield[error]);
if (error)
return error;
return std::nullopt;
}
std::optional<boost::system::error_code>
WebSocketAsyncClient::send(
boost::asio::yield_context yield,
std::string_view message,
std::chrono::steady_clock::duration timeout
)
{
auto const error = util::withTimeout(
[this, &message](auto&& cyield) { stream_.async_write(net::buffer(message), cyield); }, yield, timeout
);
if (error)
return error;
return std::nullopt;
}
std::expected<std::string, boost::system::error_code>
WebSocketAsyncClient::receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::beast::flat_buffer buffer{};
auto error =
util::withTimeout([this, &buffer](auto&& cyield) { stream_.async_read(buffer, cyield); }, yield, timeout);
if (error)
return std::unexpected{error};
return boost::beast::buffers_to_string(buffer.data());
}
void
WebSocketAsyncClient::gracefulClose(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout)
{
boost::beast::websocket::stream_base::timeout wsTimeout{};
stream_.get_option(wsTimeout);
wsTimeout.handshake_timeout = timeout;
stream_.set_option(wsTimeout);
stream_.async_close(boost::beast::websocket::close_code::normal, yield);
}
void
WebSocketAsyncClient::close()
{
boost::beast::get_lowest_layer(stream_).close();
}

View File

@@ -0,0 +1,94 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "util/TestHttpClient.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/ssl/ssl_stream.hpp>
#include <boost/beast/websocket/stream.hpp>
#include <chrono>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
class WebSocketSyncClient {
boost::asio::io_context ioc_;
boost::asio::ip::tcp::resolver resolver_{ioc_};
boost::beast::websocket::stream<boost::asio::ip::tcp::socket> ws_{ioc_};
public:
void
connect(std::string const& host, std::string const& port, std::vector<WebHeader> additionalHeaders = {});
void
disconnect();
std::string
syncPost(std::string const& body);
};
class WebSocketAsyncClient {
boost::beast::websocket::stream<boost::beast::tcp_stream> stream_;
public:
WebSocketAsyncClient(boost::asio::io_context& ioContext);
std::optional<boost::system::error_code>
connect(
std::string const& host,
std::string const& port,
boost::asio::yield_context yield,
std::chrono::steady_clock::duration timeout,
std::vector<WebHeader> additionalHeaders = {}
);
std::optional<boost::system::error_code>
send(boost::asio::yield_context yield, std::string_view message, std::chrono::steady_clock::duration timeout);
std::expected<std::string, boost::system::error_code>
receive(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout);
void
gracefulClose(boost::asio::yield_context yield, std::chrono::steady_clock::duration timeout);
void
close();
};
class WebServerSslSyncClient {
boost::asio::io_context ioc_;
std::optional<boost::beast::websocket::stream<boost::beast::ssl_stream<boost::asio::ip::tcp::socket>>> ws_;
public:
void
connect(std::string const& host, std::string const& port);
void
disconnect();
std::string
syncPost(std::string const& body);
};

View File

@@ -25,9 +25,10 @@
#include <ios>
#include <string>
#include <string_view>
#include <utility>
struct TmpFile {
std::string const path;
std::string path;
TmpFile(std::string_view content) : path{std::tmpnam(nullptr)}
{
@@ -36,8 +37,25 @@ struct TmpFile {
ofs << content;
}
TmpFile(TmpFile const&) = delete;
TmpFile(TmpFile&& other) : path{std::move(other.path)}
{
other.path.clear();
}
TmpFile&
operator=(TmpFile const&) = delete;
TmpFile&
operator=(TmpFile&& other)
{
if (this != &other)
*this = std::move(other);
return *this;
}
~TmpFile()
{
std::filesystem::remove(path);
if (not path.empty())
std::filesystem::remove(path);
}
};

View File

@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
/*
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 <boost/asio/spawn.hpp>
#include <gmock/gmock.h>
#include <chrono>
#include <memory>
#include <optional>
struct MockConnectionImpl : web::ng::Connection {
using web::ng::Connection::Connection;
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 MockConnection = testing::NiceMock<MockConnectionImpl>;
using MockConnectionPtr = std::unique_ptr<testing::NiceMock<MockConnectionImpl>>;
using StrictMockConnection = testing::StrictMock<MockConnectionImpl>;
using StrictMockConnectionPtr = std::unique_ptr<testing::StrictMock<MockConnectionImpl>>;

View File

@@ -94,6 +94,7 @@ target_sources(
rpc/RPCEngineTests.cpp
rpc/RPCHelpersTests.cpp
rpc/WorkQueueTests.cpp
test_data/SslCert.cpp
util/AccountUtilsTests.cpp
util/AssertTests.cpp
# Async framework
@@ -103,6 +104,7 @@ target_sources(
util/async/AnyStrandTests.cpp
util/async/AsyncExecutionContextTests.cpp
util/BatchingTests.cpp
util/CoroutineGroupTests.cpp
util/LedgerUtilsTests.cpp
# Prometheus support
util/prometheus/BoolTests.cpp
@@ -125,12 +127,19 @@ target_sources(
util/SignalsHandlerTests.cpp
util/TimeUtilsTests.cpp
util/TxUtilTests.cpp
util/WithTimeout.cpp
# Webserver
web/AdminVerificationTests.cpp
web/dosguard/DOSGuardTests.cpp
web/dosguard/IntervalSweepHandlerTests.cpp
web/dosguard/WhitelistHandlerTests.cpp
web/impl/ServerSslContextTests.cpp
web/impl/AdminVerificationTests.cpp
web/ng/ResponseTests.cpp
web/ng/RequestTests.cpp
web/ng/ServerTests.cpp
web/ng/impl/ConnectionHandlerTests.cpp
web/ng/impl/HttpConnectionTests.cpp
web/ng/impl/ServerSslContextTests.cpp
web/ng/impl/WsConnectionTests.cpp
web/RPCServerHandlerTests.cpp
web/ServerTests.cpp
# New Config
@@ -143,12 +152,6 @@ target_sources(
util/newconfig/ValueViewTests.cpp
)
configure_file(test_data/cert.pem ${CMAKE_BINARY_DIR}/tests/unit/test_data/cert.pem COPYONLY)
target_compile_definitions(clio_tests PRIVATE TEST_DATA_SSL_CERT_PATH="tests/unit/test_data/cert.pem")
configure_file(test_data/key.pem ${CMAKE_BINARY_DIR}/tests/unit/test_data/key.pem COPYONLY)
target_compile_definitions(clio_tests PRIVATE TEST_DATA_SSL_KEY_PATH="tests/unit/test_data/key.pem")
# See https://github.com/google/googletest/issues/3475
gtest_discover_tests(clio_tests DISCOVERY_TIMEOUT 90)

View File

@@ -0,0 +1,105 @@
//------------------------------------------------------------------------------
/*
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/TmpFile.hpp"
#include <test_data/SslCert.hpp>
#include <string_view>
namespace tests {
std::string_view
sslCert()
{
static auto constexpr CERT = R"(
-----BEGIN CERTIFICATE-----
MIIDrjCCApagAwIBAgIJAOE4Hv/P8CO3MA0GCSqGSIb3DQEBCwUAMDkxEjAQBgNV
BAMMCTEyNy4wLjAuMTELMAkGA1UEBhMCVVMxFjAUBgNVBAcMDVNhbiBGcmFuc2lz
Y28wHhcNMjMwNTE4MTUwMzEwWhcNMjQwNTE3MTUwMzEwWjBrMQswCQYDVQQGEwJV
UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjbzEN
MAsGA1UECgwEVGVzdDEMMAoGA1UECwwDRGV2MRIwEAYDVQQDDAkxMjcuMC4wLjEw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCo/crhYMiGTrfNvFKg3y0m
pFkPdbQhYUzAKW5lyFTCwc/EQLjfaw+TnxiifKdjmca1N5IaF51KocPSAUEtxT+y
7h1KyP6SAaAnAqaI+ahCJOnMSZ2DYqquevDpACKXKHIyCOjqVg6IKwtTap2ddw3w
A5oAP3C2o11ygUVAkP29T24oDzF6/AgXs6ClTIRGWePkgtMaXDM6vUihyGnEbTwk
PbYL1mVIsHYNMZtbjHw692hsC0K0pT7H2FFuBoA3+OAfN74Ks3cGrjxFjZLnU979
WsOdMBagMn9VUW+/zPieIALl1gKgB0Hpm63XVtROymqnwxa3eDMSndnVwqzzd+1p
AgMBAAGjgYYwgYMwUwYDVR0jBEwwSqE9pDswOTESMBAGA1UEAwwJMTI3LjAuMC4x
MQswCQYDVQQGEwJVUzEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjb4IJAKu2wr50Pfbq
MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuCCTEyNy4wLjAuMTAN
BgkqhkiG9w0BAQsFAAOCAQEArEjC1DmJ6q0735PxGkOmjWNsfnw8c2Zl1Z4idKfn
svEFtegNLU7tCu4aKunxlCHWiFVpunr4X67qH1JiE93W0JADnRrPxvywiqR6nUcO
p6HII/kzOizUXk59QMc1GLIIR6LDlNEeDlUbIc2DH8DPrRFBuIMYy4lf18qyfiUb
8Jt8nLeAzbhA21wI6BVhEt8G/cgIi88mPifXq+YVHrJE01jUREHRwl/MMildqxgp
LLuOOuPuy2d+HqjKE7z00j28Uf7gZK29bGx1rK+xH6veAr4plKBavBr8WWpAoUG+
PAMNb1i80cMsjK98xXDdr+7Uvy5M4COMwA5XHmMZDEW8Jw==
-----END CERTIFICATE-----
)";
return CERT;
}
TmpFile
sslCertFile()
{
return TmpFile{sslCert()};
}
std::string_view
sslKey()
{
static auto constexpr KEY = R"(
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAqP3K4WDIhk63zbxSoN8tJqRZD3W0IWFMwCluZchUwsHPxEC4
32sPk58YonynY5nGtTeSGhedSqHD0gFBLcU/su4dSsj+kgGgJwKmiPmoQiTpzEmd
g2Kqrnrw6QAilyhyMgjo6lYOiCsLU2qdnXcN8AOaAD9wtqNdcoFFQJD9vU9uKA8x
evwIF7OgpUyERlnj5ILTGlwzOr1IochpxG08JD22C9ZlSLB2DTGbW4x8OvdobAtC
tKU+x9hRbgaAN/jgHze+CrN3Bq48RY2S51Pe/VrDnTAWoDJ/VVFvv8z4niAC5dYC
oAdB6Zut11bUTspqp8MWt3gzEp3Z1cKs83ftaQIDAQABAoIBAGXZH48Zz4DyrGA4
YexG1WV2o55np/p+M82Uqs55IGyIdnmnMESmt6qWtjgnvJKQuWu6ZDmJhejW+bf1
vZyiRrPGQq0x2guRIz6foFLpdHj42lee/mmS659gxRUIWdCUNc7mA8pHt1Zl6tuJ
ZBjlCedfpE8F7R6F8unx8xTozaRr4ZbOVnqB8YWjyuIDUnujsxKdKFASZJAEzRjh
+lScXAdEYTaswgTWFFGKzwTjH/Yfv4y3LwE0RmR/1e+eQmQ7Z4C0HhjYe3EYXAvk
naH2QFZaYVhu7x/+oLPetIzFJOZn61iDhUtGYdvQVvF8qQCPqeuKeLcS9X5my9aK
nfLUryECgYEA3ZZGffe6Me6m0ZX/zwT5NbZpZCJgeALGLZPg9qulDVf8zHbDRsdn
K6Mf/Xhy3DCfSwdwcuAKz/r+4tPFyNUJR+Y2ltXaVl72iY3uJRdriNrEbZ47Ez4z
dhtEmDrD7C+7AusErEgjas+AKXkp1tovXrXUiVfRytBtoKqrym4IjJUCgYEAwzxz
fTuE2nrIwFkvg0p9PtrCwkw8dnzhBeNnzFdPOVAiHCfnNcaSOWWTkGHIkGLoORqs
fqfZCD9VkqRwsPDaSSL7vhX3oHuerDipdxOjaXVjYa7YjM6gByzo62hnG6BcQHC7
zrj7iqjnMdyNLtXcPu6zm/j5iIOLWXMevK/OVIUCgYAey4e4cfk6f0RH1GTczIAl
6tfyxqRJiXkpVGfrYCdsF1JWyBqTd5rrAZysiVTNLSS2NK54CJL4HJXXyD6wjorf
pyrnA4l4f3Ib49G47exP9Ldf1KG5JufX/iomTeR0qp1+5lKb7tqdOYFCQkiCR4hV
zUdgXwgU+6qArbd6RpiBkQKBgQCSen5jjQ5GJS0NM1y0cmS5jcPlpvEOLO9fTZiI
9VCZPYf5++46qHr42T73aoXh3nNAtMSKWkA5MdtwJDPwbSQ5Dyg1G6IoI9eOewya
LH/EFbC0j0wliLkD6SvvwurpDU1pg6tElAEVrVeYT1MVupp+FPVopkoBpEAeooKD
KpvxSQKBgQDP9fNJIpuX3kaudb0pI1OvuqBYTrTExMx+JMR+Sqf0HUwavpeCn4du
O2R4tGOOkGAX/0/actRXptFk23ucHnSIwcW6HYgDM3tDBP7n3GYdu5CSE1eiR5k7
Zl3fuvbMYcmYKgutFcRj+8NvzRWT2suzGU2x4PiPX+fh5kpvmMdvLA==
-----END RSA PRIVATE KEY-----
)";
return KEY;
}
TmpFile
sslKeyFile()
{
return TmpFile{sslKey()};
}
} // namespace tests

View File

@@ -17,32 +17,24 @@
*/
//==============================================================================
#include "web/impl/ServerSslContext.hpp"
#pragma once
#include <gtest/gtest.h>
#include "util/TmpFile.hpp"
using namespace web::impl;
#include <string_view>
TEST(ServerSslContext, makeServerSslContext)
{
auto const sslContext = makeServerSslContext(TEST_DATA_SSL_CERT_PATH, TEST_DATA_SSL_KEY_PATH);
ASSERT_TRUE(sslContext);
}
namespace tests {
TEST(ServerSslContext, makeServerSslContext_WrongCertPath)
{
auto const sslContext = makeServerSslContext("wrong_path", TEST_DATA_SSL_KEY_PATH);
ASSERT_FALSE(sslContext);
}
std::string_view
sslCert();
TEST(ServerSslContext, makeServerSslContext_WrongKeyPath)
{
auto const sslContext = makeServerSslContext(TEST_DATA_SSL_CERT_PATH, "wrong_path");
ASSERT_FALSE(sslContext);
}
TmpFile
sslCertFile();
TEST(ServerSslContext, makeServerSslContext_CertKeyMismatch)
{
auto const sslContext = makeServerSslContext(TEST_DATA_SSL_KEY_PATH, TEST_DATA_SSL_CERT_PATH);
ASSERT_FALSE(sslContext);
}
std::string_view
sslKey();
TmpFile
sslKeyFile();
} // namespace tests

View File

@@ -1,22 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDrjCCApagAwIBAgIJAOE4Hv/P8CO3MA0GCSqGSIb3DQEBCwUAMDkxEjAQBgNV
BAMMCTEyNy4wLjAuMTELMAkGA1UEBhMCVVMxFjAUBgNVBAcMDVNhbiBGcmFuc2lz
Y28wHhcNMjMwNTE4MTUwMzEwWhcNMjQwNTE3MTUwMzEwWjBrMQswCQYDVQQGEwJV
UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjbzEN
MAsGA1UECgwEVGVzdDEMMAoGA1UECwwDRGV2MRIwEAYDVQQDDAkxMjcuMC4wLjEw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCo/crhYMiGTrfNvFKg3y0m
pFkPdbQhYUzAKW5lyFTCwc/EQLjfaw+TnxiifKdjmca1N5IaF51KocPSAUEtxT+y
7h1KyP6SAaAnAqaI+ahCJOnMSZ2DYqquevDpACKXKHIyCOjqVg6IKwtTap2ddw3w
A5oAP3C2o11ygUVAkP29T24oDzF6/AgXs6ClTIRGWePkgtMaXDM6vUihyGnEbTwk
PbYL1mVIsHYNMZtbjHw692hsC0K0pT7H2FFuBoA3+OAfN74Ks3cGrjxFjZLnU979
WsOdMBagMn9VUW+/zPieIALl1gKgB0Hpm63XVtROymqnwxa3eDMSndnVwqzzd+1p
AgMBAAGjgYYwgYMwUwYDVR0jBEwwSqE9pDswOTESMBAGA1UEAwwJMTI3LjAuMC4x
MQswCQYDVQQGEwJVUzEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjb4IJAKu2wr50Pfbq
MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuCCTEyNy4wLjAuMTAN
BgkqhkiG9w0BAQsFAAOCAQEArEjC1DmJ6q0735PxGkOmjWNsfnw8c2Zl1Z4idKfn
svEFtegNLU7tCu4aKunxlCHWiFVpunr4X67qH1JiE93W0JADnRrPxvywiqR6nUcO
p6HII/kzOizUXk59QMc1GLIIR6LDlNEeDlUbIc2DH8DPrRFBuIMYy4lf18qyfiUb
8Jt8nLeAzbhA21wI6BVhEt8G/cgIi88mPifXq+YVHrJE01jUREHRwl/MMildqxgp
LLuOOuPuy2d+HqjKE7z00j28Uf7gZK29bGx1rK+xH6veAr4plKBavBr8WWpAoUG+
PAMNb1i80cMsjK98xXDdr+7Uvy5M4COMwA5XHmMZDEW8Jw==
-----END CERTIFICATE-----

View File

@@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAqP3K4WDIhk63zbxSoN8tJqRZD3W0IWFMwCluZchUwsHPxEC4
32sPk58YonynY5nGtTeSGhedSqHD0gFBLcU/su4dSsj+kgGgJwKmiPmoQiTpzEmd
g2Kqrnrw6QAilyhyMgjo6lYOiCsLU2qdnXcN8AOaAD9wtqNdcoFFQJD9vU9uKA8x
evwIF7OgpUyERlnj5ILTGlwzOr1IochpxG08JD22C9ZlSLB2DTGbW4x8OvdobAtC
tKU+x9hRbgaAN/jgHze+CrN3Bq48RY2S51Pe/VrDnTAWoDJ/VVFvv8z4niAC5dYC
oAdB6Zut11bUTspqp8MWt3gzEp3Z1cKs83ftaQIDAQABAoIBAGXZH48Zz4DyrGA4
YexG1WV2o55np/p+M82Uqs55IGyIdnmnMESmt6qWtjgnvJKQuWu6ZDmJhejW+bf1
vZyiRrPGQq0x2guRIz6foFLpdHj42lee/mmS659gxRUIWdCUNc7mA8pHt1Zl6tuJ
ZBjlCedfpE8F7R6F8unx8xTozaRr4ZbOVnqB8YWjyuIDUnujsxKdKFASZJAEzRjh
+lScXAdEYTaswgTWFFGKzwTjH/Yfv4y3LwE0RmR/1e+eQmQ7Z4C0HhjYe3EYXAvk
naH2QFZaYVhu7x/+oLPetIzFJOZn61iDhUtGYdvQVvF8qQCPqeuKeLcS9X5my9aK
nfLUryECgYEA3ZZGffe6Me6m0ZX/zwT5NbZpZCJgeALGLZPg9qulDVf8zHbDRsdn
K6Mf/Xhy3DCfSwdwcuAKz/r+4tPFyNUJR+Y2ltXaVl72iY3uJRdriNrEbZ47Ez4z
dhtEmDrD7C+7AusErEgjas+AKXkp1tovXrXUiVfRytBtoKqrym4IjJUCgYEAwzxz
fTuE2nrIwFkvg0p9PtrCwkw8dnzhBeNnzFdPOVAiHCfnNcaSOWWTkGHIkGLoORqs
fqfZCD9VkqRwsPDaSSL7vhX3oHuerDipdxOjaXVjYa7YjM6gByzo62hnG6BcQHC7
zrj7iqjnMdyNLtXcPu6zm/j5iIOLWXMevK/OVIUCgYAey4e4cfk6f0RH1GTczIAl
6tfyxqRJiXkpVGfrYCdsF1JWyBqTd5rrAZysiVTNLSS2NK54CJL4HJXXyD6wjorf
pyrnA4l4f3Ib49G47exP9Ldf1KG5JufX/iomTeR0qp1+5lKb7tqdOYFCQkiCR4hV
zUdgXwgU+6qArbd6RpiBkQKBgQCSen5jjQ5GJS0NM1y0cmS5jcPlpvEOLO9fTZiI
9VCZPYf5++46qHr42T73aoXh3nNAtMSKWkA5MdtwJDPwbSQ5Dyg1G6IoI9eOewya
LH/EFbC0j0wliLkD6SvvwurpDU1pg6tElAEVrVeYT1MVupp+FPVopkoBpEAeooKD
KpvxSQKBgQDP9fNJIpuX3kaudb0pI1OvuqBYTrTExMx+JMR+Sqf0HUwavpeCn4du
O2R4tGOOkGAX/0/actRXptFk23ucHnSIwcW6HYgDM3tDBP7n3GYdu5CSE1eiR5k7
Zl3fuvbMYcmYKgutFcRj+8NvzRWT2suzGU2x4PiPX+fh5kpvmMdvLA==
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,167 @@
//------------------------------------------------------------------------------
/*
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/CoroutineGroup.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
using namespace util;
struct CoroutineGroupTests : SyncAsioContextTest {
testing::StrictMock<testing::MockFunction<void()>> callback1_;
testing::StrictMock<testing::MockFunction<void()>> callback2_;
testing::StrictMock<testing::MockFunction<void()>> callback3_;
};
TEST_F(CoroutineGroupTests, SpawnWait)
{
testing::Sequence sequence;
EXPECT_CALL(callback1_, Call).InSequence(sequence);
EXPECT_CALL(callback2_, Call).InSequence(sequence);
EXPECT_CALL(callback3_, Call).InSequence(sequence);
runSpawn([this](boost::asio::yield_context yield) {
CoroutineGroup group{yield, 2};
group.spawn(yield, [&](boost::asio::yield_context yield) {
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}};
timer.async_wait(yield);
callback1_.Call();
});
EXPECT_EQ(group.size(), 1);
group.spawn(yield, [&](boost::asio::yield_context yield) {
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{2}};
timer.async_wait(yield);
callback2_.Call();
});
EXPECT_EQ(group.size(), 2);
group.asyncWait(yield);
EXPECT_EQ(group.size(), 0);
callback3_.Call();
});
}
TEST_F(CoroutineGroupTests, SpawnWaitSpawnWait)
{
testing::Sequence sequence;
EXPECT_CALL(callback1_, Call).InSequence(sequence);
EXPECT_CALL(callback2_, Call).InSequence(sequence);
EXPECT_CALL(callback3_, Call).InSequence(sequence);
runSpawn([this](boost::asio::yield_context yield) {
CoroutineGroup group{yield, 2};
group.spawn(yield, [&](boost::asio::yield_context yield) {
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}};
timer.async_wait(yield);
callback1_.Call();
});
EXPECT_EQ(group.size(), 1);
group.asyncWait(yield);
EXPECT_EQ(group.size(), 0);
group.spawn(yield, [&](boost::asio::yield_context yield) {
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}};
timer.async_wait(yield);
callback2_.Call();
});
EXPECT_EQ(group.size(), 1);
group.asyncWait(yield);
EXPECT_EQ(group.size(), 0);
callback3_.Call();
});
}
TEST_F(CoroutineGroupTests, ChildCoroutinesFinishBeforeWait)
{
testing::Sequence sequence;
EXPECT_CALL(callback2_, Call).InSequence(sequence);
EXPECT_CALL(callback1_, Call).InSequence(sequence);
EXPECT_CALL(callback3_, Call).InSequence(sequence);
runSpawn([this](boost::asio::yield_context yield) {
CoroutineGroup group{yield, 2};
group.spawn(yield, [&](boost::asio::yield_context yield) {
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{2}};
timer.async_wait(yield);
callback1_.Call();
});
group.spawn(yield, [&](boost::asio::yield_context yield) {
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{1}};
timer.async_wait(yield);
callback2_.Call();
});
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{3}};
timer.async_wait(yield);
group.asyncWait(yield);
callback3_.Call();
});
}
TEST_F(CoroutineGroupTests, EmptyGroup)
{
EXPECT_CALL(callback1_, Call);
runSpawn([this](boost::asio::yield_context yield) {
CoroutineGroup group{yield};
group.asyncWait(yield);
callback1_.Call();
});
}
TEST_F(CoroutineGroupTests, TooManyCoroutines)
{
EXPECT_CALL(callback1_, Call);
EXPECT_CALL(callback2_, Call);
EXPECT_CALL(callback3_, Call);
runSpawn([this](boost::asio::yield_context yield) {
CoroutineGroup group{yield, 1};
EXPECT_TRUE(group.spawn(yield, [this](boost::asio::yield_context innerYield) {
boost::asio::steady_timer timer{innerYield.get_executor(), std::chrono::milliseconds{1}};
timer.async_wait(innerYield);
callback1_.Call();
}));
EXPECT_FALSE(group.spawn(yield, [this](boost::asio::yield_context) { callback2_.Call(); }));
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{2}};
timer.async_wait(yield);
EXPECT_TRUE(group.spawn(yield, [this](boost::asio::yield_context) { callback2_.Call(); }));
group.asyncWait(yield);
callback3_.Call();
});
}

View File

@@ -18,8 +18,8 @@
//==============================================================================
#include "util/AsioContextTestFixture.hpp"
#include "util/CallWithTimeout.hpp"
#include "util/Repeat.hpp"
#include "util/WithTimeout.hpp"
#include <boost/asio/executor_work_guard.hpp>
#include <gmock/gmock.h>
@@ -41,7 +41,7 @@ struct RepeatTests : SyncAsioContextTest {
void
withRunningContext(std::function<void()> func)
{
tests::common::util::withTimeout(std::chrono::seconds{1000}, [this, func = std::move(func)]() {
tests::common::util::callWithTimeout(std::chrono::seconds{1}, [this, func = std::move(func)]() {
auto workGuard = boost::asio::make_work_guard(ctx);
std::thread thread{[this]() { ctx.run(); }};
func();

View File

@@ -0,0 +1,77 @@
//------------------------------------------------------------------------------
/*
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/WithTimeout.hpp"
#include "util/AsioContextTestFixture.hpp"
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
struct WithTimeoutTests : SyncAsioContextTest {
using CYieldType = boost::asio::cancellation_slot_binder<
boost::asio::basic_yield_context<boost::asio::any_io_executor>,
boost::asio::cancellation_slot>;
testing::StrictMock<testing::MockFunction<void(CYieldType)>> operationMock;
};
TEST_F(WithTimeoutTests, CallsOperation)
{
EXPECT_CALL(operationMock, Call);
runSpawn([&](boost::asio::yield_context yield) {
auto const error = util::withTimeout(operationMock.AsStdFunction(), yield, std::chrono::seconds{1});
EXPECT_EQ(error, boost::system::error_code{});
});
}
TEST_F(WithTimeoutTests, TimesOut)
{
EXPECT_CALL(operationMock, Call).WillOnce([](auto cyield) {
boost::asio::steady_timer timer{boost::asio::get_associated_executor(cyield)};
timer.expires_after(std::chrono::milliseconds{10});
timer.async_wait(cyield);
});
runSpawn([&](boost::asio::yield_context yield) {
auto error = util::withTimeout(operationMock.AsStdFunction(), yield, std::chrono::milliseconds{1});
EXPECT_EQ(error.value(), boost::system::errc::timed_out);
});
}
TEST_F(WithTimeoutTests, OperationFailed)
{
EXPECT_CALL(operationMock, Call).WillOnce([](auto cyield) {
boost::asio::ip::tcp::socket socket{boost::asio::get_associated_executor(cyield)};
socket.async_send(boost::asio::buffer("test"), cyield);
});
runSpawn([&](boost::asio::yield_context yield) {
auto error = util::withTimeout(operationMock.AsStdFunction(), yield, std::chrono::seconds{1});
EXPECT_EQ(error.value(), boost::system::errc::bad_file_descriptor);
});
}

View File

@@ -20,7 +20,9 @@
#include "util/AssignRandomPort.hpp"
#include "util/LoggerFixtures.hpp"
#include "util/MockPrometheus.hpp"
#include "util/TestHttpSyncClient.hpp"
#include "util/TestHttpClient.hpp"
#include "util/TestWebSocketClient.hpp"
#include "util/TmpFile.hpp"
#include "util/config/Config.hpp"
#include "util/prometheus/Gauge.hpp"
#include "util/prometheus/Label.hpp"
@@ -45,6 +47,7 @@
#include <fmt/core.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <test_data/SslCert.hpp>
#include <condition_variable>
#include <cstdint>
@@ -103,14 +106,6 @@ generateJSONDataOverload(std::string_view port)
));
}
boost::json::value
addSslConfig(boost::json::value config)
{
config.as_object()["ssl_key_file"] = TEST_DATA_SSL_KEY_PATH;
config.as_object()["ssl_cert_file"] = TEST_DATA_SSL_CERT_PATH;
return config;
}
struct WebServerTest : NoLoggerFixture {
~WebServerTest() override
{
@@ -126,6 +121,14 @@ struct WebServerTest : NoLoggerFixture {
runner.emplace([this] { ctx.run(); });
}
boost::json::value
addSslConfig(boost::json::value config) const
{
config.as_object()["ssl_key_file"] = sslKeyFile.path;
config.as_object()["ssl_cert_file"] = sslCertFile.path;
return config;
}
// this ctx is for dos timer
boost::asio::io_context ctxSync;
std::string const port = std::to_string(tests::util::generateFreePort());
@@ -141,6 +144,9 @@ struct WebServerTest : NoLoggerFixture {
// this ctx is for http server
boost::asio::io_context ctx;
TmpFile sslCertFile{tests::sslCertFile()};
TmpFile sslKeyFile{tests::sslKeyFile()};
private:
std::optional<boost::asio::io_service::work> work;
std::optional<std::thread> runner;
@@ -212,7 +218,7 @@ TEST_F(WebServerTestsWithMockPrometheus, Http)
{
auto e = std::make_shared<EchoExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuard, e);
auto const res = HttpSyncClient::syncPost("localhost", port, R"({"Hello":1})");
auto const res = HttpSyncClient::post("localhost", port, R"({"Hello":1})");
EXPECT_EQ(res, R"({"Hello":1})");
}
@@ -236,7 +242,7 @@ TEST_F(WebServerTestsWithMockPrometheus, HttpInternalError)
{
auto e = std::make_shared<ExceptionExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuard, e);
auto const res = HttpSyncClient::syncPost("localhost", port, R"({})");
auto const res = HttpSyncClient::post("localhost", port, R"({})");
EXPECT_EQ(
res,
R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})"
@@ -286,7 +292,7 @@ TEST_F(WebServerTestsWithMockPrometheus, IncompleteSslConfig)
auto e = std::make_shared<EchoExecutor>();
auto jsonConfig = generateJSONWithDynamicPort(port);
jsonConfig.as_object()["ssl_key_file"] = TEST_DATA_SSL_KEY_PATH;
jsonConfig.as_object()["ssl_key_file"] = sslKeyFile.path;
auto const server = makeServerSync(Config{jsonConfig}, ctx, dosGuard, e);
EXPECT_EQ(server, nullptr);
@@ -297,7 +303,7 @@ TEST_F(WebServerTestsWithMockPrometheus, WrongSslConfig)
auto e = std::make_shared<EchoExecutor>();
auto jsonConfig = generateJSONWithDynamicPort(port);
jsonConfig.as_object()["ssl_key_file"] = TEST_DATA_SSL_KEY_PATH;
jsonConfig.as_object()["ssl_key_file"] = sslKeyFile.path;
jsonConfig.as_object()["ssl_cert_file"] = "wrong_path";
auto const server = makeServerSync(Config{jsonConfig}, ctx, dosGuard, e);
@@ -334,9 +340,9 @@ TEST_F(WebServerTestsWithMockPrometheus, HttpRequestOverload)
{
auto e = std::make_shared<EchoExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuardOverload, e);
auto res = HttpSyncClient::syncPost("localhost", port, R"({})");
auto res = HttpSyncClient::post("localhost", port, R"({})");
EXPECT_EQ(res, "{}");
res = HttpSyncClient::syncPost("localhost", port, R"({})");
res = HttpSyncClient::post("localhost", port, R"({})");
EXPECT_EQ(
res,
R"({"error":"slowDown","error_code":10,"error_message":"You are placing too much load on the server.","status":"error","type":"response"})"
@@ -372,7 +378,7 @@ TEST_F(WebServerTestsWithMockPrometheus, HttpPayloadOverload)
std::string const s100(100, 'a');
auto e = std::make_shared<EchoExecutor>();
auto server = makeServerSync(cfg, ctx, dosGuardOverload, e);
auto const res = HttpSyncClient::syncPost("localhost", port, fmt::format(R"({{"payload":"{}"}})", s100));
auto const res = HttpSyncClient::post("localhost", port, fmt::format(R"({{"payload":"{}"}})", s100));
EXPECT_EQ(
res,
R"({"payload":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","warning":"load","warnings":[{"id":2003,"message":"You are about to be rate limited"}]})"
@@ -535,7 +541,7 @@ TEST_P(WebServerAdminTest, HttpAdminCheck)
auto server = makeServerSync(serverConfig, ctx, dosGuardOverload, e);
std::string const request = "Why hello";
uint32_t const webServerPort = serverConfig.value<uint32_t>("server.port");
auto const res = HttpSyncClient::syncPost("localhost", std::to_string(webServerPort), request, GetParam().headers);
auto const res = HttpSyncClient::post("localhost", std::to_string(webServerPort), request, GetParam().headers);
EXPECT_EQ(res, fmt::format("{} {}", request, GetParam().expectedResponse));
}
@@ -653,7 +659,7 @@ TEST_F(WebServerPrometheusTest, rejectedWithoutAdminPassword)
uint32_t const webServerPort = tests::util::generateFreePort();
Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword(webServerPort))};
auto server = makeServerSync(serverConfig, ctx, dosGuard, e);
auto const res = HttpSyncClient::syncGet("localhost", std::to_string(webServerPort), "", "/metrics");
auto const res = HttpSyncClient::get("localhost", std::to_string(webServerPort), "", "/metrics");
EXPECT_EQ(res, "Only admin is allowed to collect metrics");
}
@@ -676,7 +682,7 @@ TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled)
Config const serverConfig{boost::json::parse(JSONServerConfigWithDisabledPrometheus)};
PrometheusService::init(serverConfig);
auto server = makeServerSync(serverConfig, ctx, dosGuard, e);
auto const res = HttpSyncClient::syncGet(
auto const res = HttpSyncClient::get(
"localhost",
std::to_string(webServerPort),
"",
@@ -697,7 +703,7 @@ TEST_F(WebServerPrometheusTest, validResponse)
auto e = std::make_shared<EchoExecutor>();
Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword(webServerPort))};
auto server = makeServerSync(serverConfig, ctx, dosGuard, e);
auto const res = HttpSyncClient::syncGet(
auto const res = HttpSyncClient::get(
"localhost",
std::to_string(webServerPort),
"",

View File

@@ -18,16 +18,17 @@
//==============================================================================
#include "util/LoggerFixtures.hpp"
#include "util/config/Config.hpp"
#include "web/impl/AdminVerificationStrategy.hpp"
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/json/parse.hpp>
#include <gtest/gtest.h>
#include <optional>
#include <string>
#include <utility>
namespace http = boost::beast::http;
@@ -81,16 +82,7 @@ TEST_F(PasswordAdminVerificationStrategyTest, IsAdminReturnsTrueOnlyForValidPass
}
struct MakeAdminVerificationStrategyTestParams {
MakeAdminVerificationStrategyTestParams(
std::optional<std::string> passwordOpt,
bool expectIpStrategy,
bool expectPasswordStrategy
)
: passwordOpt(std::move(passwordOpt))
, expectIpStrategy(expectIpStrategy)
, expectPasswordStrategy(expectPasswordStrategy)
{
}
std::string testName;
std::optional<std::string> passwordOpt;
bool expectIpStrategy;
bool expectPasswordStrategy;
@@ -111,8 +103,78 @@ INSTANTIATE_TEST_CASE_P(
MakeAdminVerificationStrategyTest,
MakeAdminVerificationStrategyTest,
testing::Values(
MakeAdminVerificationStrategyTestParams(std::nullopt, true, false),
MakeAdminVerificationStrategyTestParams("p", false, true),
MakeAdminVerificationStrategyTestParams("", false, true)
MakeAdminVerificationStrategyTestParams{
.testName = "NoPassword",
.passwordOpt = std::nullopt,
.expectIpStrategy = true,
.expectPasswordStrategy = false
},
MakeAdminVerificationStrategyTestParams{
.testName = "HasPassword",
.passwordOpt = "p",
.expectIpStrategy = false,
.expectPasswordStrategy = true
},
MakeAdminVerificationStrategyTestParams{
.testName = "EmptyPassword",
.passwordOpt = "",
.expectIpStrategy = false,
.expectPasswordStrategy = true
}
)
);
struct MakeAdminVerificationStrategyFromConfigTestParams {
std::string testName;
std::string config;
bool expectedError;
};
struct MakeAdminVerificationStrategyFromConfigTest
: public testing::TestWithParam<MakeAdminVerificationStrategyFromConfigTestParams> {};
TEST_P(MakeAdminVerificationStrategyFromConfigTest, ChecksConfig)
{
util::Config serverConfig{boost::json::parse(GetParam().config)};
auto const result = web::impl::make_AdminVerificationStrategy(serverConfig);
if (GetParam().expectedError) {
EXPECT_FALSE(result.has_value());
}
}
INSTANTIATE_TEST_SUITE_P(
MakeAdminVerificationStrategyFromConfigTest,
MakeAdminVerificationStrategyFromConfigTest,
testing::Values(
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "NoPasswordNoLocalAdmin",
.config = "{}",
.expectedError = true
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "OnlyPassword",
.config = R"({"admin_password": "password"})",
.expectedError = false
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "OnlyLocalAdmin",
.config = R"({"local_admin": true})",
.expectedError = false
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "OnlyLocalAdminDisabled",
.config = R"({"local_admin": false})",
.expectedError = true
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "LocalAdminAndPassword",
.config = R"({"local_admin": true, "admin_password": "password"})",
.expectedError = true
},
MakeAdminVerificationStrategyFromConfigTestParams{
.testName = "LocalAdminDisabledAndPassword",
.config = R"({"local_admin": false, "admin_password": "password"})",
.expectedError = false
}
)
);

View File

@@ -0,0 +1,224 @@
//------------------------------------------------------------------------------
/*
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/NameGenerator.hpp"
#include "web/ng/Request.hpp"
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <gtest/gtest.h>
#include <optional>
#include <string>
using namespace web::ng;
namespace http = boost::beast::http;
struct RequestTest : public ::testing::Test {};
struct RequestMethodTestBundle {
std::string testName;
Request request;
Request::Method expectedMethod;
};
struct RequestMethodTest : RequestTest, ::testing::WithParamInterface<RequestMethodTestBundle> {};
TEST_P(RequestMethodTest, method)
{
EXPECT_EQ(GetParam().request.method(), GetParam().expectedMethod);
}
INSTANTIATE_TEST_SUITE_P(
RequestMethodTest,
RequestMethodTest,
testing::Values(
RequestMethodTestBundle{
.testName = "HttpGet",
.request = Request{http::request<http::string_body>{http::verb::get, "/", 11}},
.expectedMethod = Request::Method::Get,
},
RequestMethodTestBundle{
.testName = "HttpPost",
.request = Request{http::request<http::string_body>{http::verb::post, "/", 11}},
.expectedMethod = Request::Method::Post,
},
RequestMethodTestBundle{
.testName = "WebSocket",
.request = Request{"websocket message", Request::HttpHeaders{}},
.expectedMethod = Request::Method::Websocket,
},
RequestMethodTestBundle{
.testName = "Unsupported",
.request = Request{http::request<http::string_body>{http::verb::acl, "/", 11}},
.expectedMethod = Request::Method::Unsupported,
}
),
tests::util::NameGenerator
);
struct RequestIsHttpTestBundle {
std::string testName;
Request request;
bool expectedIsHttp;
};
struct RequestIsHttpTest : RequestTest, testing::WithParamInterface<RequestIsHttpTestBundle> {};
TEST_P(RequestIsHttpTest, isHttp)
{
EXPECT_EQ(GetParam().request.isHttp(), GetParam().expectedIsHttp);
}
INSTANTIATE_TEST_SUITE_P(
RequestIsHttpTest,
RequestIsHttpTest,
testing::Values(
RequestIsHttpTestBundle{
.testName = "HttpRequest",
.request = Request{http::request<http::string_body>{http::verb::get, "/", 11}},
.expectedIsHttp = true,
},
RequestIsHttpTestBundle{
.testName = "WebSocketRequest",
.request = Request{"websocket message", Request::HttpHeaders{}},
.expectedIsHttp = false,
}
),
tests::util::NameGenerator
);
struct RequestAsHttpRequestTest : RequestTest {};
TEST_F(RequestAsHttpRequestTest, HttpRequest)
{
http::request<http::string_body> const httpRequest{http::verb::get, "/some", 11};
Request const request{httpRequest};
auto const maybeHttpRequest = request.asHttpRequest();
ASSERT_TRUE(maybeHttpRequest.has_value());
auto const& actualHttpRequest = maybeHttpRequest->get();
EXPECT_EQ(actualHttpRequest.method(), httpRequest.method());
EXPECT_EQ(actualHttpRequest.target(), httpRequest.target());
EXPECT_EQ(actualHttpRequest.version(), httpRequest.version());
}
TEST_F(RequestAsHttpRequestTest, WebSocketRequest)
{
Request const request{"websocket message", Request::HttpHeaders{}};
auto const maybeHttpRequest = request.asHttpRequest();
EXPECT_FALSE(maybeHttpRequest.has_value());
}
struct RequestMessageTest : RequestTest {};
TEST_F(RequestMessageTest, HttpRequest)
{
std::string const body = "some body";
http::request<http::string_body> const httpRequest{http::verb::post, "/some", 11, body};
Request const request{httpRequest};
EXPECT_EQ(request.message(), httpRequest.body());
}
TEST_F(RequestMessageTest, WebSocketRequest)
{
std::string const message = "websocket message";
Request const request{message, Request::HttpHeaders{}};
EXPECT_EQ(request.message(), message);
}
struct RequestTargetTestBundle {
std::string testName;
Request request;
std::optional<std::string> expectedTarget;
};
struct RequestTargetTest : RequestTest, ::testing::WithParamInterface<RequestTargetTestBundle> {};
TEST_P(RequestTargetTest, target)
{
auto const maybeTarget = GetParam().request.target();
EXPECT_EQ(maybeTarget, GetParam().expectedTarget);
}
INSTANTIATE_TEST_SUITE_P(
RequestTargetTest,
RequestTargetTest,
testing::Values(
RequestTargetTestBundle{
.testName = "HttpRequest",
.request = Request{http::request<http::string_body>{http::verb::get, "/some", 11}},
.expectedTarget = "/some",
},
RequestTargetTestBundle{
.testName = "WebSocketRequest",
.request = Request{"websocket message", Request::HttpHeaders{}},
.expectedTarget = std::nullopt,
}
),
tests::util::NameGenerator
);
struct RequestHeaderValueTest : RequestTest {};
TEST_F(RequestHeaderValueTest, headerValue)
{
http::request<http::string_body> httpRequest{http::verb::get, "/some", 11};
http::field const headerName = http::field::user_agent;
std::string const headerValue = "clio";
httpRequest.set(headerName, headerValue);
Request const request{httpRequest};
auto const maybeHeaderValue = request.headerValue(headerName);
ASSERT_TRUE(maybeHeaderValue.has_value());
EXPECT_EQ(maybeHeaderValue.value(), headerValue);
}
TEST_F(RequestHeaderValueTest, headerValueString)
{
http::request<http::string_body> httpRequest{http::verb::get, "/some", 11};
std::string const headerName = "Custom";
std::string const headerValue = "some value";
httpRequest.set(headerName, headerValue);
Request const request{httpRequest};
auto const maybeHeaderValue = request.headerValue(headerName);
ASSERT_TRUE(maybeHeaderValue.has_value());
EXPECT_EQ(maybeHeaderValue.value(), headerValue);
}
TEST_F(RequestHeaderValueTest, headerValueNotFound)
{
http::request<http::string_body> httpRequest{http::verb::get, "/some", 11};
Request const request{httpRequest};
auto const maybeHeaderValue = request.headerValue(http::field::user_agent);
EXPECT_FALSE(maybeHeaderValue.has_value());
}
TEST_F(RequestHeaderValueTest, headerValueWebsocketRequest)
{
Request::HttpHeaders headers;
http::field const headerName = http::field::user_agent;
std::string const headerValue = "clio";
headers.set(headerName, headerValue);
Request const request{"websocket message", headers};
auto const maybeHeaderValue = request.headerValue(headerName);
ASSERT_TRUE(maybeHeaderValue.has_value());
EXPECT_EQ(maybeHeaderValue.value(), headerValue);
}

View File

@@ -0,0 +1,126 @@
//------------------------------------------------------------------------------
/*
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/build/Build.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.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/serialize.hpp>
#include <fmt/core.h>
#include <fmt/format.h>
#include <gtest/gtest.h>
#include <string>
#include <utility>
using namespace web::ng;
namespace http = boost::beast::http;
struct ResponseDeathTest : testing::Test {};
TEST_F(ResponseDeathTest, intoHttpResponseWithoutHttpData)
{
Request const request{"some messsage", Request::HttpHeaders{}};
web::ng::Response response{boost::beast::http::status::ok, "message", request};
EXPECT_DEATH(std::move(response).intoHttpResponse(), "");
}
TEST_F(ResponseDeathTest, asConstBufferWithHttpData)
{
Request const request{http::request<http::string_body>{http::verb::get, "/", 11}};
web::ng::Response response{boost::beast::http::status::ok, "message", request};
EXPECT_DEATH(response.asConstBuffer(), "");
}
struct ResponseTest : testing::Test {
int const httpVersion_ = 11;
http::status const responseStatus_ = http::status::ok;
};
TEST_F(ResponseTest, intoHttpResponse)
{
Request const request{http::request<http::string_body>{http::verb::post, "/", httpVersion_, "some message"}};
std::string const responseMessage = "response message";
web::ng::Response response{responseStatus_, responseMessage, request};
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), responseStatus_);
EXPECT_EQ(httpResponse.body(), responseMessage);
EXPECT_EQ(httpResponse.version(), httpVersion_);
EXPECT_EQ(httpResponse.keep_alive(), request.asHttpRequest()->get().keep_alive());
ASSERT_GT(httpResponse.count(http::field::content_type), 0);
EXPECT_EQ(httpResponse[http::field::content_type], "text/html");
ASSERT_GT(httpResponse.count(http::field::content_type), 0);
EXPECT_EQ(httpResponse[http::field::server], fmt::format("clio-server-{}", util::build::getClioVersionString()));
}
TEST_F(ResponseTest, intoHttpResponseJson)
{
Request const request{http::request<http::string_body>{http::verb::post, "/", httpVersion_, "some message"}};
boost::json::object const responseMessage{{"key", "value"}};
web::ng::Response response{responseStatus_, responseMessage, request};
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), responseStatus_);
EXPECT_EQ(httpResponse.body(), boost::json::serialize(responseMessage));
EXPECT_EQ(httpResponse.version(), httpVersion_);
EXPECT_EQ(httpResponse.keep_alive(), request.asHttpRequest()->get().keep_alive());
ASSERT_GT(httpResponse.count(http::field::content_type), 0);
EXPECT_EQ(httpResponse[http::field::content_type], "application/json");
ASSERT_GT(httpResponse.count(http::field::content_type), 0);
EXPECT_EQ(httpResponse[http::field::server], fmt::format("clio-server-{}", util::build::getClioVersionString()));
}
TEST_F(ResponseTest, asConstBuffer)
{
Request const request("some request", Request::HttpHeaders{});
std::string const responseMessage = "response message";
web::ng::Response response{responseStatus_, responseMessage, request};
auto const buffer = response.asConstBuffer();
EXPECT_EQ(buffer.size(), responseMessage.size());
std::string const messageFromBuffer{static_cast<char const*>(buffer.data()), buffer.size()};
EXPECT_EQ(messageFromBuffer, responseMessage);
}
TEST_F(ResponseTest, asConstBufferJson)
{
Request const request("some request", Request::HttpHeaders{});
boost::json::object const responseMessage{{"key", "value"}};
web::ng::Response response{responseStatus_, responseMessage, request};
auto const buffer = response.asConstBuffer();
EXPECT_EQ(buffer.size(), boost::json::serialize(responseMessage).size());
std::string const messageFromBuffer{static_cast<char const*>(buffer.data()), buffer.size()};
EXPECT_EQ(messageFromBuffer, boost::json::serialize(responseMessage));
}

View File

@@ -0,0 +1,332 @@
//------------------------------------------------------------------------------
/*
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/AssignRandomPort.hpp"
#include "util/LoggerFixtures.hpp"
#include "util/NameGenerator.hpp"
#include "util/Taggable.hpp"
#include "util/TestHttpClient.hpp"
#include "util/TestWebSocketClient.hpp"
#include "util/config/Config.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/Server.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/address_v4.hpp>
#include <boost/asio/ip/tcp.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 <chrono>
#include <cstdint>
#include <optional>
#include <ranges>
#include <string>
#include <utility>
using namespace web::ng;
namespace http = boost::beast::http;
struct MakeServerTestBundle {
std::string testName;
std::string configJson;
bool expectSuccess;
};
struct MakeServerTest : NoLoggerFixture, testing::WithParamInterface<MakeServerTestBundle> {
boost::asio::io_context ioContext_;
};
TEST_P(MakeServerTest, Make)
{
util::Config const config{boost::json::parse(GetParam().configJson)};
auto const expectedServer = make_Server(config, ioContext_);
EXPECT_EQ(expectedServer.has_value(), GetParam().expectSuccess);
}
INSTANTIATE_TEST_CASE_P(
MakeServerTests,
MakeServerTest,
testing::Values(
MakeServerTestBundle{
"NoIp",
R"json(
{
"server": {"port": 12345}
}
)json",
false
},
MakeServerTestBundle{
"BadEndpoint",
R"json(
{
"server": {"ip": "wrong", "port": 12345}
}
)json",
false
},
MakeServerTestBundle{
"PortMissing",
R"json(
{
"server": {"ip": "127.0.0.1"}
}
)json",
false
},
MakeServerTestBundle{
"BadSslConfig",
R"json(
{
"server": {"ip": "127.0.0.1", "port": 12345},
"ssl_cert_file": "somг_file"
}
)json",
false
},
MakeServerTestBundle{
"BadProcessingPolicy",
R"json(
{
"server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "wrong"}
}
)json",
false
},
MakeServerTestBundle{
"CorrectConfig_ParallelPolicy",
R"json(
{
"server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "parallel"}
}
)json",
true
},
MakeServerTestBundle{
"CorrectConfig_SequentPolicy",
R"json(
{
"server": {"ip": "127.0.0.1", "port": 12345, "processing_policy": "sequent"}
}
)json",
true
}
),
tests::util::NameGenerator
);
struct ServerTest : SyncAsioContextTest {
ServerTest()
{
[&]() { ASSERT_TRUE(server_.has_value()); }();
server_->onGet("/", getHandler_.AsStdFunction());
server_->onPost("/", postHandler_.AsStdFunction());
server_->onWs(wsHandler_.AsStdFunction());
}
uint32_t const serverPort_ = tests::util::generateFreePort();
util::Config const config_{
boost::json::object{{"server", boost::json::object{{"ip", "127.0.0.1"}, {"port", serverPort_}}}}
};
std::expected<Server, std::string> server_ = make_Server(config_, ctx);
std::string requestMessage_ = "some request";
std::string const headerName_ = "Some-header";
std::string const headerValue_ = "some value";
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
getHandler_;
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
postHandler_;
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
wsHandler_;
};
TEST_F(ServerTest, BadEndpoint)
{
boost::asio::ip::tcp::endpoint endpoint{boost::asio::ip::address_v4::from_string("1.2.3.4"), 0};
impl::ConnectionHandler connectionHandler{impl::ConnectionHandler::ProcessingPolicy::Sequential, std::nullopt};
util::TagDecoratorFactory tagDecoratorFactory{util::Config{boost::json::value{}}};
Server server{ctx, endpoint, std::nullopt, std::move(connectionHandler), tagDecoratorFactory};
auto maybeError = server.run();
ASSERT_TRUE(maybeError.has_value());
EXPECT_THAT(*maybeError, testing::HasSubstr("Error creating TCP acceptor"));
}
struct ServerHttpTestBundle {
std::string testName;
http::verb method;
Request::Method
expectedMethod() const
{
switch (method) {
case http::verb::get:
return Request::Method::Get;
case http::verb::post:
return Request::Method::Post;
default:
return Request::Method::Unsupported;
}
}
};
struct ServerHttpTest : ServerTest, testing::WithParamInterface<ServerHttpTestBundle> {};
TEST_F(ServerHttpTest, ClientDisconnects)
{
HttpAsyncClient client{ctx};
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
client.disconnect();
ctx.stop();
});
server_->run();
runContext();
}
TEST_P(ServerHttpTest, RequestResponse)
{
HttpAsyncClient client{ctx};
http::request<http::string_body> request{GetParam().method, "/", 11, requestMessage_};
request.set(headerName_, headerValue_);
Response const response{http::status::ok, "some response", Request{request}};
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
maybeError = client.send(request, yield, std::chrono::milliseconds{100});
EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
auto const expectedResponse = client.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }();
EXPECT_EQ(expectedResponse->result(), http::status::ok);
EXPECT_EQ(expectedResponse->body(), response.message());
}
client.gracefulShutdown();
ctx.stop();
});
auto& handler = GetParam().method == http::verb::get ? getHandler_ : postHandler_;
EXPECT_CALL(handler, Call)
.Times(3)
.WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&) {
EXPECT_TRUE(receivedRequest.isHttp());
EXPECT_EQ(receivedRequest.method(), GetParam().expectedMethod());
EXPECT_EQ(receivedRequest.message(), request.body());
EXPECT_EQ(receivedRequest.target(), request.target());
EXPECT_EQ(receivedRequest.headerValue(headerName_), request.at(headerName_));
return response;
});
server_->run();
runContext();
}
INSTANTIATE_TEST_SUITE_P(
ServerHttpTests,
ServerHttpTest,
testing::Values(ServerHttpTestBundle{"GET", http::verb::get}, ServerHttpTestBundle{"POST", http::verb::post}),
tests::util::NameGenerator
);
TEST_F(ServerTest, WsClientDisconnects)
{
WebSocketAsyncClient client{ctx};
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
client.close();
ctx.stop();
});
server_->run();
runContext();
}
TEST_F(ServerTest, WsRequestResponse)
{
WebSocketAsyncClient client{ctx};
Response const response{http::status::ok, "some response", Request{requestMessage_, Request::HttpHeaders{}}};
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto maybeError =
client.connect("127.0.0.1", std::to_string(serverPort_), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
maybeError = client.send(yield, requestMessage_, std::chrono::milliseconds{100});
EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
auto const expectedResponse = client.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << expectedResponse.error().message(); }();
EXPECT_EQ(expectedResponse.value(), response.message());
}
client.gracefulClose(yield, std::chrono::milliseconds{100});
ctx.stop();
});
EXPECT_CALL(wsHandler_, Call)
.Times(3)
.WillRepeatedly([&, response = response](Request const& receivedRequest, auto&&, auto&&) {
EXPECT_FALSE(receivedRequest.isHttp());
EXPECT_EQ(receivedRequest.method(), Request::Method::Websocket);
EXPECT_EQ(receivedRequest.message(), requestMessage_);
EXPECT_EQ(receivedRequest.target(), std::nullopt);
return response;
});
server_->run();
runContext();
}

View File

@@ -0,0 +1,453 @@
//------------------------------------------------------------------------------
/*
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/UnsupportedType.hpp"
#include "util/config/Config.hpp"
#include "web/ng/Connection.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/MockConnection.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/ConnectionHandler.hpp"
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/http/error.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/beast/websocket/error.hpp>
#include <boost/json/object.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
#include <concepts>
#include <cstddef>
#include <memory>
#include <optional>
#include <string>
#include <utility>
using namespace web::ng::impl;
using namespace web::ng;
using testing::Return;
namespace beast = boost::beast;
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}
{
}
template <typename BoostErrorType>
static std::unexpected<Error>
makeError(BoostErrorType error)
{
if constexpr (std::same_as<BoostErrorType, http::error>) {
return std::unexpected{http::make_error_code(error)};
} else if constexpr (std::same_as<BoostErrorType, websocket::error>) {
return std::unexpected{websocket::make_error_code(error)};
} else if constexpr (std::same_as<BoostErrorType, boost::asio::error::basic_errors> ||
std::same_as<BoostErrorType, boost::asio::error::misc_errors> ||
std::same_as<BoostErrorType, boost::asio::error::addrinfo_errors> ||
std::same_as<BoostErrorType, boost::asio::error::netdb_errors>) {
return std::unexpected{boost::asio::error::make_error_code(error)};
} else {
static_assert(util::Unsupported<BoostErrorType>, "Wrong error type");
}
}
template <typename... Args>
static std::expected<Request, Error>
makeRequest(Args&&... args)
{
return Request{std::forward<Args>(args)...};
}
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_);
};
struct ConnectionHandlerSequentialProcessingTest : ConnectionHandlerTest {
ConnectionHandlerSequentialProcessingTest()
: ConnectionHandlerTest(ConnectionHandler::ProcessingPolicy::Sequential, std::nullopt)
{
}
};
TEST_F(ConnectionHandlerSequentialProcessingTest, ReceiveError)
{
EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(http::error::end_of_stream)));
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, ReceiveError_CloseConnection)
{
EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(boost::asio::error::timed_out)));
EXPECT_CALL(*mockConnection_, close);
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_NoHandler_Send)
{
EXPECT_CALL(*mockConnection_, receive)
.WillOnce(Return(makeRequest("some_request", Request::HttpHeaders{})))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(*mockConnection_, 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);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadTarget_Send)
{
std::string const target = "/some/target";
std::string const requestMessage = "some message";
EXPECT_CALL(*mockConnection_, 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_EQ(response.message(), "Bad target");
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::bad_request);
EXPECT_EQ(httpResponse.version(), 11);
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadMethod_Send)
{
EXPECT_CALL(*mockConnection_, 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_EQ(response.message(), "Unsupported http method");
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, 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)
.WillOnce(Return(makeRequest(requestMessage, Request::HttpHeaders{})))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, 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_EQ(response.message(), responseMessage);
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send_Loop)
{
std::string const target = "/some/target";
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, 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(*mockConnection_, 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_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(*mockConnection_, close);
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), 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)>>
getHandlerMock;
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
connectionHandler_.onGet(target, getHandlerMock.AsStdFunction());
EXPECT_CALL(*mockConnection_, 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_EQ(request.message(), requestMessage);
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockConnection_, 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);
});
}
TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, 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)
.Times(4)
.WillRepeatedly([&](auto&&, auto&&) -> std::expected<Request, Error> {
if (connectionClosed) {
return makeError(websocket::error::closed);
}
return makeRequest(requestMessage, Request::HttpHeaders{});
});
EXPECT_CALL(wsHandlerMock, Call).Times(3).WillRepeatedly([&](Request const& request, 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_EQ(response.message(), responseMessage);
++numCalls;
if (numCalls == 3)
connectionHandler_.stop();
return std::nullopt;
});
EXPECT_CALL(*mockConnection_, close).WillOnce([&connectionClosed]() { connectionClosed = true; });
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
struct ConnectionHandlerParallelProcessingTest : ConnectionHandlerTest {
static size_t constexpr maxParallelRequests = 3;
ConnectionHandlerParallelProcessingTest()
: ConnectionHandlerTest(
ConnectionHandler::ProcessingPolicy::Parallel,
ConnectionHandlerParallelProcessingTest::maxParallelRequests
)
{
}
static void
asyncSleep(boost::asio::yield_context yield, std::chrono::steady_clock::duration duration)
{
boost::asio::steady_timer timer{yield.get_executor()};
timer.expires_after(duration);
timer.async_wait(yield);
}
};
TEST_F(ConnectionHandlerParallelProcessingTest, ReceiveError)
{
EXPECT_CALL(*mockConnection_, receive).WillOnce(Return(makeError(http::error::end_of_stream)));
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, 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)
.WillOnce(Return(makeRequest(requestMessage, Request::HttpHeaders{})))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(wsHandlerMock, Call).WillOnce([&](Request const& request, 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_EQ(response.message(), responseMessage);
return std::nullopt;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
auto const returnRequest = [&](auto&&, auto&&) { return makeRequest(requestMessage, Request::HttpHeaders{}); };
EXPECT_CALL(*mockConnection_, receive)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(wsHandlerMock, Call).Times(2).WillRepeatedly([&](Request const& request, 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;
});
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}
TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooManyRequest)
{
testing::StrictMock<testing::MockFunction<Response(Request const&, ConnectionContext, boost::asio::yield_context)>>
wsHandlerMock;
connectionHandler_.onWs(wsHandlerMock.AsStdFunction());
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
auto const returnRequest = [&](auto&&, auto&&) { return makeRequest(requestMessage, Request::HttpHeaders{}); };
testing::Sequence sequence;
EXPECT_CALL(*mockConnection_, receive)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(returnRequest)
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(wsHandlerMock, Call)
.Times(3)
.WillRepeatedly([&](Request const& request, 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_,
send(
testing::ResultOf([](Response response) { return response.message(); }, responseMessage),
testing::_,
testing::_
)
)
.Times(3)
.WillRepeatedly(Return(std::nullopt));
EXPECT_CALL(
*mockConnection_,
send(
testing::ResultOf(
[](Response response) { return response.message(); }, "Too many requests for one connection"
),
testing::_,
testing::_
)
)
.Times(2)
.WillRepeatedly(Return(std::nullopt));
runSpawn([this](boost::asio::yield_context yield) {
connectionHandler_.processConnection(std::move(mockConnection_), yield);
});
}

View File

@@ -0,0 +1,296 @@
//------------------------------------------------------------------------------
/*
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/TestHttpClient.hpp"
#include "util/TestHttpServer.hpp"
#include "util/TestWebSocketClient.hpp"
#include "util/config/Config.hpp"
#include "web/ng/Request.hpp"
#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>
#include <boost/beast/core/flat_buffer.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 <gtest/gtest.h>
#include <chrono>
#include <cstddef>
#include <optional>
#include <ranges>
#include <utility>
using namespace web::ng::impl;
using namespace web::ng;
namespace http = boost::beast::http;
struct HttpConnectionTests : SyncAsioContextTest {
util::TagDecoratorFactory tagDecoratorFactory_{util::Config{boost::json::object{{"log_tag_style", "int"}}}};
TestHttpServer httpServer_{ctx, "localhost"};
HttpAsyncClient httpClient_{ctx};
http::request<http::string_body> request_{http::verb::post, "/some_target", 11, "some data"};
PlainHttpConnection
acceptConnection(boost::asio::yield_context yield)
{
auto expectedSocket = httpServer_.accept(yield);
[&]() { ASSERT_TRUE(expectedSocket.has_value()) << expectedSocket.error().message(); }();
auto ip = expectedSocket->remote_endpoint().address().to_string();
return PlainHttpConnection{
std::move(expectedSocket).value(), std::move(ip), boost::beast::flat_buffer{}, tagDecoratorFactory_
};
}
};
TEST_F(HttpConnectionTests, wasUpgraded)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
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_FALSE(connection.wasUpgraded());
});
}
TEST_F(HttpConnectionTests, Receive)
{
request_.set(boost::beast::http::field::user_agent, "test_client");
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
maybeError = httpClient_.send(request_, 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();
auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{100});
ASSERT_TRUE(expectedRequest.has_value()) << expectedRequest.error().message();
ASSERT_TRUE(expectedRequest->isHttp());
auto const& receivedRequest = expectedRequest.value().asHttpRequest()->get();
EXPECT_EQ(receivedRequest.method(), request_.method());
EXPECT_EQ(receivedRequest.target(), request_.target());
EXPECT_EQ(receivedRequest.body(), request_.body());
EXPECT_EQ(
receivedRequest.at(boost::beast::http::field::user_agent),
request_.at(boost::beast::http::field::user_agent)
);
});
}
TEST_F(HttpConnectionTests, ReceiveTimeout)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{1});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
});
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{1});
EXPECT_FALSE(expectedRequest.has_value());
});
}
TEST_F(HttpConnectionTests, ReceiveClientDisconnected)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{1});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
httpClient_.disconnect();
});
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{1});
EXPECT_FALSE(expectedRequest.has_value());
});
}
TEST_F(HttpConnectionTests, Send)
{
Request const request{request_};
Response const response{http::status::ok, "some response data", request};
boost::asio::spawn(ctx, [this, response = response](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(); }();
auto const expectedResponse = httpClient_.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << maybeError->message(); }();
auto const receivedResponse = expectedResponse.value();
auto const sentResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(receivedResponse.result(), sentResponse.result());
EXPECT_EQ(receivedResponse.body(), sentResponse.body());
EXPECT_EQ(receivedResponse.version(), request_.version());
EXPECT_TRUE(receivedResponse.keep_alive());
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto maybeError = connection.send(response, yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
});
}
TEST_F(HttpConnectionTests, SendMultipleTimes)
{
Request const request{request_};
Response const response{http::status::ok, "some response data", request};
boost::asio::spawn(ctx, [this, response = response](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(); }();
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
auto const expectedResponse = httpClient_.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResponse.has_value()) << maybeError->message(); }();
auto const receivedResponse = expectedResponse.value();
auto const sentResponse = Response{response}.intoHttpResponse();
EXPECT_EQ(receivedResponse.result(), sentResponse.result());
EXPECT_EQ(receivedResponse.body(), sentResponse.body());
EXPECT_EQ(receivedResponse.version(), request_.version());
EXPECT_TRUE(receivedResponse.keep_alive());
}
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
auto maybeError = connection.send(response, yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
}
});
}
TEST_F(HttpConnectionTests, SendClientDisconnected)
{
Response const response{http::status::ok, "some response data", Request{request_}};
boost::asio::spawn(ctx, [this, response = response](boost::asio::yield_context yield) mutable {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{1});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
httpClient_.disconnect();
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto maybeError = connection.send(response, yield, std::chrono::milliseconds{1});
size_t counter{1};
while (not maybeError.has_value() and counter < 100) {
++counter;
maybeError = connection.send(response, yield, std::chrono::milliseconds{1});
}
EXPECT_TRUE(maybeError.has_value());
EXPECT_LT(counter, 100);
});
}
TEST_F(HttpConnectionTests, Close)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
size_t counter{0};
while (not maybeError.has_value() and counter < 100) {
++counter;
maybeError = httpClient_.send(request_, yield, std::chrono::milliseconds{1});
}
EXPECT_TRUE(maybeError.has_value());
EXPECT_LT(counter, 100);
});
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
connection.close(yield, std::chrono::milliseconds{1});
});
}
TEST_F(HttpConnectionTests, IsUpgradeRequested_GotHttpRequest)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = httpClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
maybeError = httpClient_.send(request_, yield, std::chrono::milliseconds{1});
EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
});
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto result = connection.isUpgradeRequested(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(result.has_value()) << result.error().message(); }();
EXPECT_FALSE(result.value());
});
}
TEST_F(HttpConnectionTests, IsUpgradeRequested_FailedToFetch)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
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);
auto result = connection.isUpgradeRequested(yield, std::chrono::milliseconds{1});
EXPECT_FALSE(result.has_value());
});
}
TEST_F(HttpConnectionTests, Upgrade)
{
WebSocketAsyncClient wsClient_{ctx};
boost::asio::spawn(ctx, [this, &wsClient_](boost::asio::yield_context yield) {
auto maybeError = wsClient_.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);
auto const expectedResult = connection.isUpgradeRequested(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedResult.has_value()) << expectedResult.error().message(); }();
[&]() { ASSERT_TRUE(expectedResult.value()); }();
std::optional<boost::asio::ssl::context> sslContext;
auto expectedWsConnection = connection.upgrade(sslContext, tagDecoratorFactory_, yield);
[&]() { ASSERT_TRUE(expectedWsConnection.has_value()) << expectedWsConnection.error().message(); }();
});
}

View File

@@ -0,0 +1,181 @@
//------------------------------------------------------------------------------
/*
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/NameGenerator.hpp"
#include "util/TmpFile.hpp"
#include "util/config/Config.hpp"
#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>
#include <optional>
#include <string>
using namespace web::ng::impl;
struct MakeServerSslContextFromConfigTestBundle {
std::string testName;
std::optional<std::string> certFile;
std::optional<std::string> keyFile;
std::optional<std::string> expectedError;
bool expectContext;
boost::json::value
configJson() const
{
boost::json::object result;
if (certFile.has_value()) {
result["ssl_cert_file"] = *certFile;
}
if (keyFile.has_value()) {
result["ssl_key_file"] = *keyFile;
}
return result;
}
};
struct MakeServerSslContextFromConfigTest : testing::TestWithParam<MakeServerSslContextFromConfigTestBundle> {};
TEST_P(MakeServerSslContextFromConfigTest, makeFromConfig)
{
auto const config = util::Config{GetParam().configJson()};
auto const expectedServerSslContext = makeServerSslContext(config);
if (GetParam().expectedError.has_value()) {
ASSERT_FALSE(expectedServerSslContext.has_value());
EXPECT_THAT(expectedServerSslContext.error(), testing::HasSubstr(*GetParam().expectedError));
} else {
EXPECT_EQ(expectedServerSslContext.value().has_value(), GetParam().expectContext);
}
}
INSTANTIATE_TEST_SUITE_P(
MakeServerSslContextFromConfigTest,
MakeServerSslContextFromConfigTest,
testing::ValuesIn(
{MakeServerSslContextFromConfigTestBundle{
.testName = "NoCertNoKey",
.certFile = std::nullopt,
.keyFile = std::nullopt,
.expectedError = std::nullopt,
.expectContext = false
},
MakeServerSslContextFromConfigTestBundle{
.testName = "CertOnly",
.certFile = "some_path",
.keyFile = std::nullopt,
.expectedError = "Config entries 'ssl_cert_file' and 'ssl_key_file' must be set or unset together.",
.expectContext = false
},
MakeServerSslContextFromConfigTestBundle{
.testName = "KeyOnly",
.certFile = std::nullopt,
.keyFile = "some_path",
.expectedError = "Config entries 'ssl_cert_file' and 'ssl_key_file' must be set or unset together.",
.expectContext = false
},
MakeServerSslContextFromConfigTestBundle{
.testName = "BothKeyAndCert",
.certFile = "some_path",
.keyFile = "some_other_path",
.expectedError = "Can't read SSL certificate",
.expectContext = false
}}
),
tests::util::NameGenerator
);
struct MakeServerSslContextFromConfigRealFilesTest : testing::Test {};
TEST_F(MakeServerSslContextFromConfigRealFilesTest, WrongKeyFile)
{
auto const certFile = tests::sslCertFile();
boost::json::object configJson = {{"ssl_cert_file", certFile.path}, {"ssl_key_file", "some_path"}};
util::Config const config{configJson};
auto const expectedServerSslContext = makeServerSslContext(config);
ASSERT_FALSE(expectedServerSslContext.has_value());
EXPECT_THAT(expectedServerSslContext.error(), testing::HasSubstr("Can't read SSL key"));
}
TEST_F(MakeServerSslContextFromConfigRealFilesTest, BothFilesValid)
{
auto const certFile = tests::sslCertFile();
auto const keyFile = tests::sslKeyFile();
boost::json::object configJson = {{"ssl_cert_file", certFile.path}, {"ssl_key_file", keyFile.path}};
util::Config const config{configJson};
auto const expectedServerSslContext = makeServerSslContext(config);
EXPECT_TRUE(expectedServerSslContext.has_value());
}
struct MakeServerSslContextFromDataTestBundle {
std::string testName;
std::string certData;
std::string keyData;
bool expectedSuccess;
};
struct MakeServerSslContextFromDataTest : testing::TestWithParam<MakeServerSslContextFromDataTestBundle> {};
TEST_P(MakeServerSslContextFromDataTest, makeFromData)
{
auto const& data = GetParam();
auto const expectedServerSslContext = makeServerSslContext(data.certData, data.keyData);
EXPECT_EQ(expectedServerSslContext.has_value(), data.expectedSuccess);
}
INSTANTIATE_TEST_SUITE_P(
MakeServerSslContextFromDataTest,
MakeServerSslContextFromDataTest,
testing::ValuesIn(
{MakeServerSslContextFromDataTestBundle{
.testName = "EmptyData",
.certData = "",
.keyData = "",
.expectedSuccess = false
},
MakeServerSslContextFromDataTestBundle{
.testName = "CertOnly",
.certData = std::string{tests::sslCert()},
.keyData = "",
.expectedSuccess = false
},
MakeServerSslContextFromDataTestBundle{
.testName = "KeyOnly",
.certData = "",
.keyData = std::string{tests::sslKey()},
.expectedSuccess = false
},
MakeServerSslContextFromDataTestBundle{
.testName = "BothKeyAndCert",
.certData = std::string{tests::sslCert()},
.keyData = std::string{tests::sslKey()},
.expectedSuccess = true
}}
),
tests::util::NameGenerator
);

View File

@@ -0,0 +1,250 @@
//------------------------------------------------------------------------------
/*
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/TestHttpServer.hpp"
#include "util/TestWebSocketClient.hpp"
#include "util/config/Config.hpp"
#include "web/ng/Error.hpp"
#include "web/ng/Request.hpp"
#include "web/ng/Response.hpp"
#include "web/ng/impl/HttpConnection.hpp"
#include "web/ng/impl/WsConnection.hpp"
#include <boost/asio/error.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/websocket/error.hpp>
#include <boost/json/object.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
#include <cstddef>
#include <memory>
#include <optional>
#include <ranges>
#include <utility>
using namespace web::ng::impl;
using namespace web::ng;
struct web_WsConnectionTests : SyncAsioContextTest {
util::TagDecoratorFactory tagDecoratorFactory_{util::Config{boost::json::object{{"log_tag_style", "int"}}}};
TestHttpServer httpServer_{ctx, "localhost"};
WebSocketAsyncClient wsClient_{ctx};
Request request_{"some request", Request::HttpHeaders{}};
std::unique_ptr<PlainWsConnection>
acceptConnection(boost::asio::yield_context yield)
{
auto expectedSocket = httpServer_.accept(yield);
[&]() { ASSERT_TRUE(expectedSocket.has_value()) << expectedSocket.error().message(); }();
auto ip = expectedSocket->remote_endpoint().address().to_string();
PlainHttpConnection httpConnection{
std::move(expectedSocket).value(), std::move(ip), boost::beast::flat_buffer{}, tagDecoratorFactory_
};
auto expectedTrue = httpConnection.isUpgradeRequested(yield);
[&]() {
ASSERT_TRUE(expectedTrue.has_value()) << expectedTrue.error().message();
ASSERT_TRUE(expectedTrue.value()) << "Expected upgrade request";
}();
std::optional<boost::asio::ssl::context> sslContext;
auto expectedWsConnection = httpConnection.upgrade(sslContext, tagDecoratorFactory_, yield);
[&]() { ASSERT_TRUE(expectedWsConnection.has_value()) << expectedWsConnection.error().message(); }();
auto connection = std::move(expectedWsConnection).value();
auto wsConnectionPtr = dynamic_cast<PlainWsConnection*>(connection.release());
[&]() { ASSERT_NE(wsConnectionPtr, nullptr) << "Expected PlainWsConnection"; }();
return std::unique_ptr<PlainWsConnection>{wsConnectionPtr};
}
};
TEST_F(web_WsConnectionTests, WasUpgraded)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
});
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
EXPECT_TRUE(wsConnection->wasUpgraded());
});
}
TEST_F(web_WsConnectionTests, Send)
{
Response const response{boost::beast::http::status::ok, "some response", request_};
boost::asio::spawn(ctx, [this, &response](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
auto const expectedMessage = wsClient_.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedMessage.has_value()) << expectedMessage.error().message(); }();
EXPECT_EQ(expectedMessage.value(), response.message());
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
auto maybeError = wsConnection->send(response, yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
});
}
TEST_F(web_WsConnectionTests, MultipleSend)
{
Response const response{boost::beast::http::status::ok, "some response", request_};
boost::asio::spawn(ctx, [this, &response](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
auto const expectedMessage = wsClient_.receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(expectedMessage.has_value()) << expectedMessage.error().message(); }();
EXPECT_EQ(expectedMessage.value(), response.message());
}
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
auto maybeError = wsConnection->send(response, yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
}
});
}
TEST_F(web_WsConnectionTests, SendFailed)
{
Response const response{boost::beast::http::status::ok, "some response", request_};
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
wsClient_.close();
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
std::optional<Error> maybeError;
size_t counter = 0;
while (not maybeError.has_value() and counter < 100) {
maybeError = wsConnection->send(response, yield, std::chrono::milliseconds{1});
++counter;
}
EXPECT_TRUE(maybeError.has_value());
EXPECT_LT(counter, 100);
});
}
TEST_F(web_WsConnectionTests, Receive)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
maybeError = wsClient_.send(yield, request_.message(), std::chrono::milliseconds{100});
EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
});
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(maybeRequest.has_value()) << maybeRequest.error().message(); }();
EXPECT_EQ(maybeRequest->message(), request_.message());
});
}
TEST_F(web_WsConnectionTests, MultipleReceive)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
maybeError = wsClient_.send(yield, request_.message(), std::chrono::milliseconds{100});
EXPECT_FALSE(maybeError.has_value()) << maybeError->message();
}
});
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{100});
[&]() { ASSERT_TRUE(maybeRequest.has_value()) << maybeRequest.error().message(); }();
EXPECT_EQ(maybeRequest->message(), request_.message());
}
});
}
TEST_F(web_WsConnectionTests, ReceiveTimeout)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
});
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{1});
EXPECT_FALSE(maybeRequest.has_value());
EXPECT_EQ(maybeRequest.error().value(), boost::asio::error::timed_out);
});
}
TEST_F(web_WsConnectionTests, ReceiveFailed)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
wsClient_.close();
});
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{100});
EXPECT_FALSE(maybeRequest.has_value());
EXPECT_EQ(maybeRequest.error().value(), boost::asio::error::eof);
});
}
TEST_F(web_WsConnectionTests, Close)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
auto const maybeMessage = wsClient_.receive(yield, std::chrono::milliseconds{100});
EXPECT_FALSE(maybeMessage.has_value());
EXPECT_THAT(maybeMessage.error().message(), testing::HasSubstr("was gracefully closed"));
});
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
wsConnection->close(yield, std::chrono::milliseconds{100});
});
}