mirror of
https://github.com/XRPLF/clio.git
synced 2025-12-06 17:27:58 +00:00
Requests library (#1140)
For #51. First part of improving forwarding - library for easy async requests.
This commit is contained in:
124
unittests/util/TestHttpServer.cpp
Normal file
124
unittests/util/TestHttpServer.cpp
Normal file
@@ -0,0 +1,124 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/TestHttpServer.h"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/socket_base.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/beast.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/beast/http/error.hpp>
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/message_generator.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace http = beast::http;
|
||||
namespace asio = boost::asio;
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
namespace {
|
||||
|
||||
void
|
||||
doSession(beast::tcp_stream stream, TestHttpServer::RequestHandler requestHandler, asio::yield_context yield)
|
||||
{
|
||||
beast::error_code errorCode;
|
||||
|
||||
// This buffer is required to persist across reads
|
||||
beast::flat_buffer buffer;
|
||||
|
||||
// This lambda is used to send messages
|
||||
// Set the timeout.
|
||||
stream.expires_after(std::chrono::seconds(5));
|
||||
|
||||
// Read a request
|
||||
http::request<http::string_body> req;
|
||||
http::async_read(stream, buffer, req, yield[errorCode]);
|
||||
if (errorCode == http::error::end_of_stream)
|
||||
return;
|
||||
|
||||
ASSERT_FALSE(errorCode) << errorCode.message();
|
||||
|
||||
auto response = requestHandler(req);
|
||||
|
||||
if (not response)
|
||||
return;
|
||||
|
||||
bool const keep_alive = response->keep_alive();
|
||||
|
||||
http::message_generator messageGenerator{std::move(response).value()};
|
||||
|
||||
// Send the response
|
||||
beast::async_write(stream, std::move(messageGenerator), yield[errorCode]);
|
||||
|
||||
ASSERT_FALSE(errorCode) << errorCode.message();
|
||||
|
||||
if (!keep_alive) {
|
||||
// This means we should close the connection, usually because
|
||||
// the response indicated the "Connection: close" semantic.
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a TCP shutdown
|
||||
stream.socket().shutdown(tcp::socket::shutdown_send, errorCode);
|
||||
|
||||
// At this point the connection is closed gracefully
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TestHttpServer::TestHttpServer(boost::asio::io_context& context, std::string host, int const port) : acceptor_(context)
|
||||
{
|
||||
boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::make_address(host), port);
|
||||
acceptor_.open(endpoint.protocol());
|
||||
acceptor_.set_option(asio::socket_base::reuse_address(true));
|
||||
acceptor_.bind(endpoint);
|
||||
acceptor_.listen(asio::socket_base::max_listen_connections);
|
||||
}
|
||||
|
||||
void
|
||||
TestHttpServer::handleRequest(TestHttpServer::RequestHandler handler)
|
||||
{
|
||||
boost::asio::spawn(
|
||||
acceptor_.get_executor(),
|
||||
[this, handler = std::move(handler)](asio::yield_context yield) mutable {
|
||||
boost::beast::error_code errorCode;
|
||||
tcp::socket socket(this->acceptor_.get_executor());
|
||||
acceptor_.async_accept(socket, yield[errorCode]);
|
||||
|
||||
[&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }();
|
||||
|
||||
doSession(beast::tcp_stream{std::move(socket)}, std::move(handler), yield);
|
||||
},
|
||||
boost::asio::detached
|
||||
);
|
||||
}
|
||||
60
unittests/util/TestHttpServer.h
Normal file
60
unittests/util/TestHttpServer.h
Normal file
@@ -0,0 +1,60 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* @brief Simple HTTP server for use in unit tests
|
||||
*/
|
||||
class TestHttpServer {
|
||||
public:
|
||||
using RequestHandler = std::function<std::optional<boost::beast::http::response<
|
||||
boost::beast::http::string_body>>(boost::beast::http::request<boost::beast::http::string_body>)>;
|
||||
|
||||
/**
|
||||
* @brief Construct a new TestHttpServer
|
||||
*
|
||||
* @param context boost::asio::io_context to use for networking
|
||||
* @param host host to bind to
|
||||
* @param port port to bind to
|
||||
*/
|
||||
TestHttpServer(boost::asio::io_context& context, std::string host, int port);
|
||||
|
||||
/**
|
||||
* @brief Start the server
|
||||
*
|
||||
* @note This method schedules to process only one request
|
||||
*
|
||||
* @param handler RequestHandler to use for incoming request
|
||||
*/
|
||||
void
|
||||
handleRequest(RequestHandler handler);
|
||||
|
||||
private:
|
||||
boost::asio::ip::tcp::acceptor acceptor_;
|
||||
};
|
||||
117
unittests/util/TestWsServer.cpp
Normal file
117
unittests/util/TestWsServer.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/TestWsServer.h"
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/socket_base.hpp>
|
||||
#include <boost/asio/spawn.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/role.hpp>
|
||||
#include <boost/beast/version.hpp>
|
||||
#include <boost/beast/websocket/error.hpp>
|
||||
#include <boost/beast/websocket/rfc6455.hpp>
|
||||
#include <boost/beast/websocket/stream_base.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace beast = boost::beast;
|
||||
namespace websocket = boost::beast::websocket;
|
||||
|
||||
TestWsConnection::TestWsConnection(asio::ip::tcp::socket&& socket, boost::asio::yield_context yield)
|
||||
: ws_(std::move(socket))
|
||||
{
|
||||
ws_.set_option(websocket::stream_base::timeout::suggested(beast::role_type::server));
|
||||
beast::error_code errorCode;
|
||||
ws_.async_accept(yield[errorCode]);
|
||||
[&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }();
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
TestWsConnection::send(std::string const& message, boost::asio::yield_context yield)
|
||||
{
|
||||
beast::error_code errorCode;
|
||||
ws_.async_write(asio::buffer(message), yield[errorCode]);
|
||||
if (errorCode)
|
||||
return errorCode.message();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
TestWsConnection::receive(boost::asio::yield_context yield)
|
||||
{
|
||||
beast::error_code errorCode;
|
||||
beast::flat_buffer buffer;
|
||||
|
||||
ws_.async_read(buffer, yield[errorCode]);
|
||||
if (errorCode == websocket::error::closed)
|
||||
return std::nullopt;
|
||||
|
||||
[&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }();
|
||||
return beast::buffers_to_string(buffer.data());
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
TestWsConnection::close(boost::asio::yield_context yield)
|
||||
{
|
||||
beast::error_code errorCode;
|
||||
ws_.async_close(websocket::close_code::normal, yield[errorCode]);
|
||||
if (errorCode)
|
||||
return errorCode.message();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
TestWsServer::TestWsServer(asio::io_context& context, std::string const& host, int port) : acceptor_(context)
|
||||
{
|
||||
auto endpoint = asio::ip::tcp::endpoint(boost::asio::ip::make_address(host), port);
|
||||
acceptor_.open(endpoint.protocol());
|
||||
acceptor_.set_option(asio::socket_base::reuse_address(true));
|
||||
acceptor_.bind(endpoint);
|
||||
}
|
||||
|
||||
TestWsConnection
|
||||
TestWsServer::acceptConnection(asio::yield_context yield)
|
||||
{
|
||||
acceptor_.listen(asio::socket_base::max_listen_connections);
|
||||
beast::error_code errorCode;
|
||||
asio::ip::tcp::socket socket(acceptor_.get_executor());
|
||||
acceptor_.async_accept(socket, yield[errorCode]);
|
||||
[&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }();
|
||||
return TestWsConnection(std::move(socket), yield);
|
||||
}
|
||||
|
||||
void
|
||||
TestWsServer::acceptConnectionAndDropIt(asio::yield_context yield)
|
||||
{
|
||||
acceptor_.listen(asio::socket_base::max_listen_connections);
|
||||
beast::error_code errorCode;
|
||||
asio::ip::tcp::socket socket(acceptor_.get_executor());
|
||||
acceptor_.async_accept(socket, yield[errorCode]);
|
||||
[&]() { ASSERT_FALSE(errorCode) << errorCode.message(); }();
|
||||
}
|
||||
64
unittests/util/TestWsServer.h
Normal file
64
unittests/util/TestWsServer.h
Normal file
@@ -0,0 +1,64 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 <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/websocket/stream.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
class TestWsConnection {
|
||||
boost::beast::websocket::stream<boost::beast::tcp_stream> ws_;
|
||||
|
||||
public:
|
||||
using SendCallback = std::function<void()>;
|
||||
using ReceiveCallback = std::function<void(std::string)>;
|
||||
|
||||
TestWsConnection(boost::asio::ip::tcp::socket&& socket, boost::asio::yield_context yield);
|
||||
|
||||
// returns error message if error occurs
|
||||
std::optional<std::string>
|
||||
send(std::string const& message, boost::asio::yield_context yield);
|
||||
|
||||
// returns nullopt if the connection is closed
|
||||
std::optional<std::string>
|
||||
receive(boost::asio::yield_context yield);
|
||||
|
||||
std::optional<std::string>
|
||||
close(boost::asio::yield_context yield);
|
||||
};
|
||||
|
||||
class TestWsServer {
|
||||
boost::asio::ip::tcp::acceptor acceptor_;
|
||||
|
||||
public:
|
||||
TestWsServer(boost::asio::io_context& context, std::string const& host, int port);
|
||||
|
||||
TestWsConnection
|
||||
acceptConnection(boost::asio::yield_context yield);
|
||||
|
||||
void
|
||||
acceptConnectionAndDropIt(boost::asio::yield_context yield);
|
||||
};
|
||||
189
unittests/util/requests/RequestBuilderTests.cpp
Normal file
189
unittests/util/requests/RequestBuilderTests.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/Expected.h"
|
||||
#include "util/Fixtures.h"
|
||||
#include "util/TestHttpServer.h"
|
||||
#include "util/requests/RequestBuilder.h"
|
||||
#include "util/requests/Types.h"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/spawn.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 <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace util::requests;
|
||||
using namespace boost;
|
||||
using namespace boost::beast;
|
||||
|
||||
struct RequestBuilderTestBundle {
|
||||
std::string testName;
|
||||
http::verb method;
|
||||
std::vector<HttpHeader> headers;
|
||||
std::string target;
|
||||
};
|
||||
|
||||
struct RequestBuilderTest : SyncAsioContextTest, testing::WithParamInterface<RequestBuilderTestBundle> {
|
||||
TestHttpServer server{ctx, "0.0.0.0", 11111};
|
||||
RequestBuilder builder{"localhost", "11111"};
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
RequestBuilderTest,
|
||||
RequestBuilderTest,
|
||||
testing::Values(
|
||||
RequestBuilderTestBundle{"GetSimple", http::verb::get, {}, "/"},
|
||||
RequestBuilderTestBundle{
|
||||
"GetWithHeaders",
|
||||
http::verb::get,
|
||||
{{http::field::accept, "text/html"}, {http::field::authorization, "password"}},
|
||||
"/"
|
||||
},
|
||||
RequestBuilderTestBundle{"GetWithTarget", http::verb::get, {}, "/test"},
|
||||
RequestBuilderTestBundle{"PostSimple", http::verb::post, {}, "/"},
|
||||
RequestBuilderTestBundle{
|
||||
"PostWithHeaders",
|
||||
http::verb::post,
|
||||
{{http::field::accept, "text/html"}, {http::field::authorization, "password"}},
|
||||
"/"
|
||||
},
|
||||
RequestBuilderTestBundle{"PostWithTarget", http::verb::post, {}, "/test"}
|
||||
),
|
||||
[](auto const& info) { return info.param.testName; }
|
||||
);
|
||||
|
||||
TEST_P(RequestBuilderTest, SimpleRequest)
|
||||
{
|
||||
std::string const replyBody = "Hello, world!";
|
||||
builder.addHeaders(GetParam().headers);
|
||||
builder.setTarget(GetParam().target);
|
||||
|
||||
server.handleRequest(
|
||||
[&replyBody](http::request<http::string_body> request) -> std::optional<http::response<http::string_body>> {
|
||||
[&]() {
|
||||
ASSERT_TRUE(request.target() == GetParam().target);
|
||||
ASSERT_TRUE(request.method() == GetParam().method);
|
||||
}();
|
||||
return http::response<http::string_body>{http::status::ok, 11, replyBody};
|
||||
}
|
||||
);
|
||||
|
||||
runSpawn([this, replyBody](asio::yield_context yield) {
|
||||
auto const response = [&]() -> util::Expected<std::string, RequestError> {
|
||||
switch (GetParam().method) {
|
||||
case http::verb::get:
|
||||
return builder.get(yield);
|
||||
case http::verb::post:
|
||||
return builder.post(yield);
|
||||
default:
|
||||
return util::Unexpected{RequestError{"Invalid HTTP verb"}};
|
||||
}
|
||||
}();
|
||||
ASSERT_TRUE(response) << response.error().message;
|
||||
EXPECT_EQ(response.value(), replyBody);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RequestBuilderTest, Timeout)
|
||||
{
|
||||
builder.setTimeout(std::chrono::milliseconds{10});
|
||||
server.handleRequest(
|
||||
[](http::request<http::string_body> request) -> std::optional<http::response<http::string_body>> {
|
||||
[&]() {
|
||||
ASSERT_TRUE(request.target() == "/");
|
||||
ASSERT_TRUE(request.method() == http::verb::get);
|
||||
}();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{20});
|
||||
return std::nullopt;
|
||||
}
|
||||
);
|
||||
runSpawn([this](asio::yield_context yield) {
|
||||
auto response = builder.get(yield);
|
||||
EXPECT_FALSE(response);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RequestBuilderTest, RequestWithBody)
|
||||
{
|
||||
std::string const requestBody = "Hello, world!";
|
||||
std::string const replyBody = "Hello, client!";
|
||||
builder.addData(requestBody);
|
||||
|
||||
server.handleRequest(
|
||||
[&](http::request<http::string_body> request) -> std::optional<http::response<http::string_body>> {
|
||||
[&]() {
|
||||
EXPECT_EQ(request.target(), "/");
|
||||
EXPECT_EQ(request.method(), http::verb::get);
|
||||
EXPECT_EQ(request.body(), requestBody);
|
||||
}();
|
||||
|
||||
return http::response<http::string_body>{http::status::ok, 11, replyBody};
|
||||
}
|
||||
);
|
||||
|
||||
runSpawn([&](asio::yield_context yield) {
|
||||
auto const response = builder.get(yield);
|
||||
ASSERT_TRUE(response) << response.error().message;
|
||||
EXPECT_EQ(response.value(), replyBody) << response.value();
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RequestBuilderTest, ResolveError)
|
||||
{
|
||||
builder = RequestBuilder{"wrong_host", "11111"};
|
||||
runSpawn([this](asio::yield_context yield) {
|
||||
auto const response = builder.get(yield);
|
||||
ASSERT_FALSE(response);
|
||||
EXPECT_TRUE(response.error().message.starts_with("Resolve error")) << response.error().message;
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RequestBuilderTest, ConnectionError)
|
||||
{
|
||||
builder = RequestBuilder{"localhost", "11112"};
|
||||
builder.setTimeout(std::chrono::milliseconds{1});
|
||||
runSpawn([this](asio::yield_context yield) {
|
||||
auto const response = builder.get(yield);
|
||||
ASSERT_FALSE(response);
|
||||
EXPECT_TRUE(response.error().message.starts_with("Connection error")) << response.error().message;
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RequestBuilderTest, WritingError)
|
||||
{
|
||||
server.handleRequest(
|
||||
[](http::request<http::string_body> request) -> std::optional<http::response<http::string_body>> {
|
||||
[&]() {
|
||||
EXPECT_EQ(request.target(), "/");
|
||||
EXPECT_EQ(request.method(), http::verb::get);
|
||||
}();
|
||||
return std::nullopt;
|
||||
}
|
||||
);
|
||||
}
|
||||
30
unittests/util/requests/SslContextTests.cpp
Normal file
30
unittests/util/requests/SslContextTests.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/requests/impl/SslContext.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace util::requests::impl;
|
||||
|
||||
TEST(SslContext, Create)
|
||||
{
|
||||
auto ctx = makeSslContext();
|
||||
EXPECT_TRUE(ctx);
|
||||
}
|
||||
236
unittests/util/requests/WsConnectionTests.cpp
Normal file
236
unittests/util/requests/WsConnectionTests.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/Fixtures.h"
|
||||
#include "util/TestWsServer.h"
|
||||
#include "util/requests/Types.h"
|
||||
#include "util/requests/WsConnection.h"
|
||||
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/beast/http/field.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace util::requests;
|
||||
namespace asio = boost::asio;
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
struct WsConnectionTestsBase : SyncAsioContextTest {
|
||||
WsConnectionBuilder builder{"localhost", "11112"};
|
||||
TestWsServer server{ctx, "0.0.0.0", 11112};
|
||||
};
|
||||
|
||||
struct WsConnectionTestBundle {
|
||||
std::string testName;
|
||||
std::vector<HttpHeader> headers;
|
||||
std::optional<std::string> target;
|
||||
};
|
||||
|
||||
struct WsConnectionTests : WsConnectionTestsBase, testing::WithParamInterface<WsConnectionTestBundle> {
|
||||
WsConnectionTests()
|
||||
{
|
||||
[this]() { ASSERT_EQ(clientMessages.size(), serverMessages.size()); }();
|
||||
}
|
||||
std::vector<std::string> const clientMessages{"hello", "world"};
|
||||
|
||||
std::vector<std::string> const serverMessages{"goodbye", "point"};
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
WsConnectionTestsGroup,
|
||||
WsConnectionTests,
|
||||
testing::Values(
|
||||
WsConnectionTestBundle{"noHeaders", {}, std::nullopt},
|
||||
WsConnectionTestBundle{"singleHeader", {{http::field::accept, "text/html"}}, std::nullopt},
|
||||
WsConnectionTestBundle{
|
||||
"multiple headers",
|
||||
{{http::field::accept, "text/html"}, {http::field::authorization, "password"}},
|
||||
std::nullopt
|
||||
},
|
||||
WsConnectionTestBundle{"target", {}, "/target"}
|
||||
)
|
||||
);
|
||||
|
||||
TEST_P(WsConnectionTests, SendAndReceive)
|
||||
{
|
||||
auto target = GetParam().target;
|
||||
if (target) {
|
||||
builder.setTarget(*target);
|
||||
}
|
||||
|
||||
for (auto const& header : GetParam().headers) {
|
||||
builder.addHeader(header);
|
||||
}
|
||||
|
||||
asio::spawn(ctx, [&](asio::yield_context yield) {
|
||||
auto serverConnection = server.acceptConnection(yield);
|
||||
|
||||
for (size_t i = 0; i < clientMessages.size(); ++i) {
|
||||
auto message = serverConnection.receive(yield);
|
||||
EXPECT_EQ(clientMessages.at(i), message);
|
||||
|
||||
auto error = serverConnection.send(serverMessages.at(i), yield);
|
||||
ASSERT_FALSE(error) << *error;
|
||||
}
|
||||
});
|
||||
|
||||
runSpawn([&](asio::yield_context yield) {
|
||||
auto maybeConnection = builder.connect(yield);
|
||||
ASSERT_TRUE(maybeConnection.has_value()) << maybeConnection.error().message;
|
||||
auto& connection = *maybeConnection;
|
||||
|
||||
for (size_t i = 0; i < serverMessages.size(); ++i) {
|
||||
auto error = connection->write(clientMessages.at(i), yield);
|
||||
ASSERT_FALSE(error) << error->message;
|
||||
|
||||
auto message = connection->read(yield);
|
||||
ASSERT_TRUE(message.has_value()) << message.error().message;
|
||||
EXPECT_EQ(serverMessages.at(i), message.value());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(WsConnectionTests, Timeout)
|
||||
{
|
||||
builder.setConnectionTimeout(std::chrono::milliseconds{1});
|
||||
runSpawn([&](asio::yield_context yield) {
|
||||
auto connection = builder.connect(yield);
|
||||
ASSERT_FALSE(connection.has_value());
|
||||
|
||||
EXPECT_TRUE(connection.error().message.starts_with("Connect error"));
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(WsConnectionTests, ResolveError)
|
||||
{
|
||||
builder = WsConnectionBuilder{"wrong_host", "11112"};
|
||||
runSpawn([&](asio::yield_context yield) {
|
||||
auto connection = builder.connect(yield);
|
||||
ASSERT_FALSE(connection.has_value());
|
||||
EXPECT_TRUE(connection.error().message.starts_with("Resolve error")) << connection.error().message;
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(WsConnectionTests, WsHandshakeError)
|
||||
{
|
||||
builder.setConnectionTimeout(std::chrono::milliseconds{1});
|
||||
asio::spawn(ctx, [&](asio::yield_context yield) { server.acceptConnectionAndDropIt(yield); });
|
||||
runSpawn([&](asio::yield_context yield) {
|
||||
auto connection = builder.connect(yield);
|
||||
ASSERT_FALSE(connection.has_value());
|
||||
EXPECT_TRUE(connection.error().message.starts_with("Handshake error")) << connection.error().message;
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(WsConnectionTests, CloseConnection)
|
||||
{
|
||||
asio::spawn(ctx, [&](asio::yield_context yield) {
|
||||
auto serverConnection = server.acceptConnection(yield);
|
||||
|
||||
auto message = serverConnection.receive(yield);
|
||||
EXPECT_EQ(std::nullopt, message);
|
||||
});
|
||||
|
||||
runSpawn([&](asio::yield_context yield) {
|
||||
auto connection = builder.connect(yield);
|
||||
ASSERT_TRUE(connection.has_value()) << connection.error().message;
|
||||
|
||||
auto error = connection->operator*().close(yield);
|
||||
EXPECT_FALSE(error.has_value()) << error->message;
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(WsConnectionTests, MultipleConnections)
|
||||
{
|
||||
for (size_t i = 0; i < 2; ++i) {
|
||||
asio::spawn(ctx, [&](asio::yield_context yield) {
|
||||
auto serverConnection = server.acceptConnection(yield);
|
||||
auto message = serverConnection.receive(yield);
|
||||
|
||||
ASSERT_TRUE(message.has_value());
|
||||
EXPECT_EQ(*message, "hello");
|
||||
});
|
||||
|
||||
runSpawn([&](asio::yield_context yield) {
|
||||
auto connection = builder.connect(yield);
|
||||
ASSERT_TRUE(connection.has_value()) << connection.error().message;
|
||||
|
||||
auto error = connection->operator*().write("hello", yield);
|
||||
ASSERT_FALSE(error) << error->message;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enum class WsConnectionErrorTestsBundle : int { Read = 1, Write = 2 };
|
||||
|
||||
struct WsConnectionErrorTests : WsConnectionTestsBase, testing::WithParamInterface<WsConnectionErrorTestsBundle> {};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
WsConnectionErrorTestsGroup,
|
||||
WsConnectionErrorTests,
|
||||
testing::Values(WsConnectionErrorTestsBundle::Read, WsConnectionErrorTestsBundle::Write),
|
||||
[](auto const& info) {
|
||||
switch (info.param) {
|
||||
case WsConnectionErrorTestsBundle::Read:
|
||||
return "Read";
|
||||
case WsConnectionErrorTestsBundle::Write:
|
||||
return "Write";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
);
|
||||
|
||||
TEST_P(WsConnectionErrorTests, WriteError)
|
||||
{
|
||||
asio::spawn(ctx, [&](asio::yield_context yield) {
|
||||
auto serverConnection = server.acceptConnection(yield);
|
||||
|
||||
auto error = serverConnection.close(yield);
|
||||
EXPECT_FALSE(error.has_value()) << *error;
|
||||
});
|
||||
|
||||
runSpawn([&](asio::yield_context yield) {
|
||||
auto maybeConnection = builder.connect(yield);
|
||||
ASSERT_TRUE(maybeConnection.has_value()) << maybeConnection.error().message;
|
||||
auto& connection = *maybeConnection;
|
||||
|
||||
auto error = connection->close(yield);
|
||||
EXPECT_FALSE(error.has_value()) << error->message;
|
||||
|
||||
switch (GetParam()) {
|
||||
case WsConnectionErrorTestsBundle::Read: {
|
||||
auto const expected = connection->read(yield);
|
||||
EXPECT_FALSE(expected.has_value());
|
||||
break;
|
||||
}
|
||||
case WsConnectionErrorTestsBundle::Write: {
|
||||
error = connection->write("hello", yield);
|
||||
EXPECT_TRUE(error.has_value());
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user