Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4

This commit is contained in:
Ed Hennis
2025-09-26 12:09:17 -04:00
committed by GitHub
6 changed files with 372 additions and 2 deletions

View File

@@ -138,6 +138,7 @@ test.toplevel > test.csf
test.toplevel > xrpl.json
test.unit_test > xrpl.basics
tests.libxrpl > xrpl.basics
tests.libxrpl > xrpl.net
xrpl.json > xrpl.basics
xrpl.ledger > xrpl.basics
xrpl.ledger > xrpl.protocol

View File

@@ -36,7 +36,7 @@ XRPL_FIX (IncludeKeyletFields, Supported::no, VoteBehavior::DefaultNo
XRPL_FEATURE(DynamicMPT, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (TokenEscrowV1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (DelegateV1_1, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (PriceOracleOrder, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (PriceOracleOrder, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (MPTDeliveredAmount, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (AMMClawbackRounding, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -383,7 +383,7 @@ public:
static boost::regex reStatus{
"\\`HTTP/1\\S+ (\\d{3}) .*\\'"}; // HTTP/1.1 200 OK
static boost::regex reSize{
"\\`.*\\r\\nContent-Length:\\s+([0-9]+).*\\'"};
"\\`.*\\r\\nContent-Length:\\s+([0-9]+).*\\'", boost::regex::icase};
static boost::regex reBody{"\\`.*\\r\\n\\r\\n(.*)\\'"};
boost::smatch smMatch;

View File

@@ -12,3 +12,5 @@ xrpl_add_test(basics)
target_link_libraries(xrpl.test.basics PRIVATE xrpl.imports.test)
xrpl_add_test(crypto)
target_link_libraries(xrpl.test.crypto PRIVATE xrpl.imports.test)
xrpl_add_test(net)
target_link_libraries(xrpl.test.net PRIVATE xrpl.imports.test)

View File

@@ -0,0 +1,346 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or 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 <xrpl/basics/Log.h>
#include <xrpl/net/HTTPClient.h>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <doctest/doctest.h>
#include <atomic>
#include <map>
#include <thread>
using namespace ripple;
namespace {
// Simple HTTP server using Beast for testing
class TestHTTPServer
{
private:
boost::asio::io_context ioc_;
boost::asio::ip::tcp::acceptor acceptor_;
boost::asio::ip::tcp::endpoint endpoint_;
std::atomic<bool> running_{true};
unsigned short port_;
// Custom headers to return
std::map<std::string, std::string> custom_headers_;
std::string response_body_;
unsigned int status_code_{200};
public:
TestHTTPServer() : acceptor_(ioc_), port_(0)
{
// Bind to any available port
endpoint_ = {boost::asio::ip::tcp::v4(), 0};
acceptor_.open(endpoint_.protocol());
acceptor_.set_option(boost::asio::socket_base::reuse_address(true));
acceptor_.bind(endpoint_);
acceptor_.listen();
// Get the actual port that was assigned
port_ = acceptor_.local_endpoint().port();
accept();
}
~TestHTTPServer()
{
stop();
}
boost::asio::io_context&
ioc()
{
return ioc_;
}
unsigned short
port() const
{
return port_;
}
void
setHeader(std::string const& name, std::string const& value)
{
custom_headers_[name] = value;
}
void
setResponseBody(std::string const& body)
{
response_body_ = body;
}
void
setStatusCode(unsigned int code)
{
status_code_ = code;
}
private:
void
stop()
{
running_ = false;
acceptor_.close();
}
void
accept()
{
if (!running_)
return;
acceptor_.async_accept(
ioc_,
endpoint_,
[&](boost::system::error_code const& error,
boost::asio::ip::tcp::socket peer) {
if (!running_)
return;
if (!error)
{
handleConnection(std::move(peer));
}
});
}
void
handleConnection(boost::asio::ip::tcp::socket socket)
{
try
{
// Read the HTTP request
boost::beast::flat_buffer buffer;
boost::beast::http::request<boost::beast::http::string_body> req;
boost::beast::http::read(socket, buffer, req);
// Create response
boost::beast::http::response<boost::beast::http::string_body> res;
res.version(req.version());
res.result(status_code_);
res.set(boost::beast::http::field::server, "TestServer");
// Add custom headers
for (auto const& [name, value] : custom_headers_)
{
res.set(name, value);
}
// Set body and prepare payload first
res.body() = response_body_;
res.prepare_payload();
// Override Content-Length with custom headers after prepare_payload
// This allows us to test case-insensitive header parsing
for (auto const& [name, value] : custom_headers_)
{
if (boost::iequals(name, "Content-Length"))
{
res.erase(boost::beast::http::field::content_length);
res.set(name, value);
}
}
// Send response
boost::beast::http::write(socket, res);
// Shutdown socket gracefully
boost::system::error_code ec;
socket.shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec);
}
catch (std::exception const&)
{
// Connection handling errors are expected
}
if (running_)
accept();
}
};
// Helper function to run HTTP client test
bool
runHTTPTest(
TestHTTPServer& server,
std::string const& path,
std::atomic<bool>& completed,
std::atomic<int>& result_status,
std::string& result_data,
boost::system::error_code& result_error)
{
// Create a null journal for testing
beast::Journal j{beast::Journal::getNullSink()};
// Initialize HTTPClient SSL context
HTTPClient::initializeSSLContext("", "", false, j);
HTTPClient::get(
false, // no SSL
server.ioc(),
"127.0.0.1",
server.port(),
path,
1024, // max response size
std::chrono::seconds(5),
[&](boost::system::error_code const& ec,
int status,
std::string const& data) -> bool {
result_error = ec;
result_status = status;
result_data = data;
completed = true;
return false; // don't retry
},
j);
// Run the IO context until completion
auto start = std::chrono::steady_clock::now();
while (!completed &&
std::chrono::steady_clock::now() - start < std::chrono::seconds(10))
{
if (server.ioc().run_one() == 0)
{
break;
}
}
return completed;
}
} // anonymous namespace
TEST_CASE("HTTPClient case insensitive Content-Length")
{
// Test different cases of Content-Length header
std::vector<std::string> header_cases = {
"Content-Length", // Standard case
"content-length", // Lowercase - this tests the regex icase fix
"CONTENT-LENGTH", // Uppercase
"Content-length", // Mixed case
"content-Length" // Mixed case 2
};
for (auto const& header_name : header_cases)
{
TestHTTPServer server;
std::string test_body = "Hello World!";
server.setResponseBody(test_body);
server.setHeader(header_name, std::to_string(test_body.size()));
std::atomic<bool> completed{false};
std::atomic<int> result_status{0};
std::string result_data;
boost::system::error_code result_error;
bool test_completed = runHTTPTest(
server,
"/test",
completed,
result_status,
result_data,
result_error);
// Verify results
CHECK(test_completed);
CHECK(!result_error);
CHECK(result_status == 200);
CHECK(result_data == test_body);
}
}
TEST_CASE("HTTPClient basic HTTP request")
{
TestHTTPServer server;
std::string test_body = "Test response body";
server.setResponseBody(test_body);
server.setHeader("Content-Type", "text/plain");
std::atomic<bool> completed{false};
std::atomic<int> result_status{0};
std::string result_data;
boost::system::error_code result_error;
bool test_completed = runHTTPTest(
server, "/basic", completed, result_status, result_data, result_error);
CHECK(test_completed);
CHECK(!result_error);
CHECK(result_status == 200);
CHECK(result_data == test_body);
}
TEST_CASE("HTTPClient empty response")
{
TestHTTPServer server;
server.setResponseBody(""); // Empty body
server.setHeader("Content-Length", "0");
std::atomic<bool> completed{false};
std::atomic<int> result_status{0};
std::string result_data;
boost::system::error_code result_error;
bool test_completed = runHTTPTest(
server, "/empty", completed, result_status, result_data, result_error);
CHECK(test_completed);
CHECK(!result_error);
CHECK(result_status == 200);
CHECK(result_data.empty());
}
TEST_CASE("HTTPClient different status codes")
{
std::vector<unsigned int> status_codes = {200, 404, 500};
for (auto status : status_codes)
{
TestHTTPServer server;
server.setStatusCode(status);
server.setResponseBody("Status " + std::to_string(status));
std::atomic<bool> completed{false};
std::atomic<int> result_status{0};
std::string result_data;
boost::system::error_code result_error;
bool test_completed = runHTTPTest(
server,
"/status",
completed,
result_status,
result_data,
result_error);
CHECK(test_completed);
CHECK(!result_error);
CHECK(result_status == static_cast<int>(status));
}
}

View File

@@ -0,0 +1,21 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or 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.
*/
//==============================================================================
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>