2
0
mirror of https://github.com/boostorg/redis.git synced 2026-01-19 04:42:09 +00:00

Removes handshaker in favor of asio::deferred (#291)

Refactors the handshake process to use asio::deferred instead of a custom composed operation.
Fixes logging on HELLO error (close #297)
Fixes a potential problem on reconnection after a HELLO error is encountered (close #290)
Fixes a race condition in the health checker that could cause it to never exit on cancellation
Adds support for users with a username different than "default" and an empty password (close #298)
Adds integration testing for authentication
Adds unit testing for the hello utility functions
This commit is contained in:
Anarthal (Rubén Pérez)
2025-09-04 16:48:00 +02:00
committed by GitHub
parent 2133ed747b
commit 0cf2441ed2
12 changed files with 454 additions and 210 deletions

View File

@@ -41,6 +41,7 @@ make_test(test_exec_fsm)
make_test(test_log_to_file)
make_test(test_conn_logging)
make_test(test_reader_fsm)
make_test(test_hello_utils)
# Tests that require a real Redis server
make_test(test_conn_quit)
@@ -55,6 +56,7 @@ make_test(test_conn_exec_cancel)
make_test(test_conn_exec_cancel2)
make_test(test_conn_echo_stress)
make_test(test_conn_move)
make_test(test_conn_auth)
make_test(test_issue_50)
make_test(test_issue_181)
make_test(test_conversions)

View File

@@ -57,6 +57,7 @@ local tests =
test_log_to_file
test_conn_logging
test_reader_fsm
test_hello_utils
;
# Build and run the tests

143
test/test_conn_auth.cpp Normal file
View File

@@ -0,0 +1,143 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/redis/connection.hpp>
#include <boost/redis/logger.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/core/lightweight_test.hpp>
#include <boost/system/error_code.hpp>
#include "common.hpp"
#include <iostream>
#include <sstream>
#include <string>
namespace asio = boost::asio;
namespace redis = boost::redis;
using namespace std::chrono_literals;
using boost::system::error_code;
namespace {
// Creates a user with a known password. Harmless if the user already exists
void setup_password()
{
// Setup
asio::io_context ioc;
redis::connection conn{ioc};
// Enable the user and grant them permissions on everything
redis::request req;
req.push("ACL", "SETUSER", "myuser", "on", ">mypass", "~*", "&*", "+@all");
redis::generic_response resp;
bool run_finished = false, exec_finished = false;
conn.async_run(make_test_config(), [&](error_code ec) {
run_finished = true;
BOOST_TEST_EQ(ec, asio::error::operation_aborted);
});
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
exec_finished = true;
BOOST_TEST_EQ(ec, error_code());
conn.cancel();
});
ioc.run_for(test_timeout);
BOOST_TEST(run_finished);
BOOST_TEST(exec_finished);
BOOST_TEST(resp.has_value());
}
void test_auth_success()
{
// Setup
asio::io_context ioc;
redis::connection conn{ioc};
// This request should return the username we're logged in as
redis::request req;
req.push("ACL", "WHOAMI");
redis::response<std::string> resp;
// These credentials are set up in main, before tests are run
auto cfg = make_test_config();
cfg.username = "myuser";
cfg.password = "mypass";
bool exec_finished = false, run_finished = false;
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
exec_finished = true;
BOOST_TEST_EQ(ec, error_code());
conn.cancel();
});
conn.async_run(cfg, [&](error_code ec) {
run_finished = true;
BOOST_TEST_EQ(ec, asio::error::operation_aborted);
});
ioc.run_for(test_timeout);
BOOST_TEST(exec_finished);
BOOST_TEST(run_finished);
BOOST_TEST_EQ(std::get<0>(resp).value(), "myuser");
}
void test_auth_failure()
{
// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297)
std::ostringstream oss;
redis::logger lgr(redis::logger::level::info, [&](redis::logger::level, std::string_view msg) {
oss << msg << '\n';
});
// Setup
asio::io_context ioc;
redis::connection conn{ioc, std::move(lgr)};
// Disable reconnection so the hello error causes the connection to exit
auto cfg = make_test_config();
cfg.username = "myuser";
cfg.password = "wrongpass"; // wrong
cfg.reconnect_wait_interval = 0s;
bool run_finished = false;
conn.async_run(cfg, [&](error_code ec) {
run_finished = true;
BOOST_TEST_EQ(ec, redis::error::resp3_hello);
});
ioc.run_for(test_timeout);
BOOST_TEST(run_finished);
// Check the log
auto log = oss.str();
if (!BOOST_TEST_NE(log.find("WRONGPASS"), std::string::npos)) {
std::cerr << "Log was: " << log << std::endl;
}
}
} // namespace
int main()
{
setup_password();
test_auth_success();
test_auth_failure();
return boost::report_errors();
}

189
test/test_hello_utils.cpp Normal file
View File

@@ -0,0 +1,189 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/redis/adapter/result.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/detail/hello_utils.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/error.hpp>
#include <boost/core/lightweight_test.hpp>
#include <boost/system/result.hpp>
namespace asio = boost::asio;
namespace redis = boost::redis;
using redis::detail::setup_hello_request;
using redis::detail::clear_response;
using redis::detail::check_hello_response;
using boost::system::error_code;
namespace {
void test_setup_hello_request()
{
redis::config cfg;
cfg.clientname = "";
redis::request req;
setup_hello_request(cfg, req);
std::string_view const expected = "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n";
BOOST_TEST_EQ(req.payload(), expected);
}
void test_setup_hello_request_select()
{
redis::config cfg;
cfg.clientname = "";
cfg.database_index = 10;
redis::request req;
setup_hello_request(cfg, req);
std::string_view const expected =
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
"*2\r\n$6\r\nSELECT\r\n$2\r\n10\r\n";
BOOST_TEST_EQ(req.payload(), expected);
}
void test_setup_hello_request_clientname()
{
redis::config cfg;
redis::request req;
setup_hello_request(cfg, req);
std::string_view const
expected = "*4\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$7\r\nSETNAME\r\n$11\r\nBoost.Redis\r\n";
BOOST_TEST_EQ(req.payload(), expected);
}
void test_setup_hello_request_auth()
{
redis::config cfg;
cfg.clientname = "";
cfg.username = "foo";
cfg.password = "bar";
redis::request req;
setup_hello_request(cfg, req);
std::string_view const
expected = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n";
BOOST_TEST_EQ(req.payload(), expected);
}
void test_setup_hello_request_auth_empty_password()
{
redis::config cfg;
cfg.clientname = "";
cfg.username = "foo";
redis::request req;
setup_hello_request(cfg, req);
std::string_view const
expected = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$0\r\n\r\n";
BOOST_TEST_EQ(req.payload(), expected);
}
void test_setup_hello_request_auth_setname()
{
redis::config cfg;
cfg.clientname = "mytest";
cfg.username = "foo";
cfg.password = "bar";
redis::request req;
setup_hello_request(cfg, req);
std::string_view const expected =
"*7\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$7\r\nSETNAME\r\n$"
"6\r\nmytest\r\n";
BOOST_TEST_EQ(req.payload(), expected);
}
// clear response
void test_clear_response_empty()
{
redis::generic_response resp;
clear_response(resp);
BOOST_TEST(resp.has_value());
BOOST_TEST_EQ(resp.value().size(), 0u);
}
void test_clear_response_nonempty()
{
redis::generic_response resp;
resp->push_back({});
clear_response(resp);
BOOST_TEST(resp.has_value());
BOOST_TEST_EQ(resp.value().size(), 0u);
}
void test_clear_response_error()
{
redis::generic_response resp{
boost::system::in_place_error,
redis::adapter::error{redis::resp3::type::blob_error, "message"}
};
clear_response(resp);
BOOST_TEST(resp.has_value());
BOOST_TEST_EQ(resp.value().size(), 0u);
}
// check response
void test_check_hello_response_success()
{
redis::generic_response resp;
resp->push_back({});
auto ec = check_hello_response(error_code(), resp);
BOOST_TEST_EQ(ec, error_code());
}
void test_check_hello_response_io_error()
{
redis::generic_response resp;
auto ec = check_hello_response(asio::error::already_open, resp);
BOOST_TEST_EQ(ec, asio::error::already_open);
}
void test_check_hello_response_server_error()
{
redis::generic_response resp{
boost::system::in_place_error,
redis::adapter::error{redis::resp3::type::simple_error, "wrong password"}
};
auto ec = check_hello_response(error_code(), resp);
BOOST_TEST_EQ(ec, redis::error::resp3_hello);
}
} // namespace
int main()
{
test_setup_hello_request();
test_setup_hello_request_select();
test_setup_hello_request_clientname();
test_setup_hello_request_auth();
test_setup_hello_request_auth_empty_password();
test_setup_hello_request_auth_setname();
test_clear_response_empty();
test_clear_response_nonempty();
test_clear_response_error();
test_check_hello_response_success();
test_check_hello_response_io_error();
test_check_hello_response_server_error();
return boost::report_errors();
}

View File

@@ -8,26 +8,24 @@
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/redis/detail/read_buffer.hpp>
#include <boost/redis/detail/resp3_handshaker.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/serialization.hpp>
#include <boost/redis/resp3/type.hpp>
#define BOOST_TEST_MODULE conn_quit
#include <boost/test/included/unit_test.hpp>
#include <boost/redis/response.hpp>
#include "common.hpp"
#define BOOST_TEST_MODULE low_level_sync_sans_io
#include <boost/test/included/unit_test.hpp>
#include <iostream>
#include <string>
using boost::redis::request;
using boost::redis::adapter::adapt2;
using boost::redis::adapter::result;
using boost::redis::config;
using boost::redis::detail::multiplexer;
using boost::redis::detail::push_hello;
using boost::redis::generic_response;
using boost::redis::ignore_t;
using boost::redis::request;
using boost::redis::resp3::detail::deserialize;
using boost::redis::resp3::node;
using boost::redis::resp3::to_string;
@@ -57,61 +55,6 @@ BOOST_AUTO_TEST_CASE(low_level_sync_sans_io)
}
}
BOOST_AUTO_TEST_CASE(config_to_hello)
{
config cfg;
cfg.clientname = "";
request req;
push_hello(cfg, req);
std::string_view const expected = "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n";
BOOST_CHECK_EQUAL(req.payload(), expected);
}
BOOST_AUTO_TEST_CASE(config_to_hello_with_select)
{
config cfg;
cfg.clientname = "";
cfg.database_index = 10;
request req;
push_hello(cfg, req);
std::string_view const expected =
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
"*2\r\n$6\r\nSELECT\r\n$2\r\n10\r\n";
BOOST_CHECK_EQUAL(req.payload(), expected);
}
BOOST_AUTO_TEST_CASE(config_to_hello_cmd_clientname)
{
config cfg;
request req;
push_hello(cfg, req);
std::string_view const
expected = "*4\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$7\r\nSETNAME\r\n$11\r\nBoost.Redis\r\n";
BOOST_CHECK_EQUAL(req.payload(), expected);
}
BOOST_AUTO_TEST_CASE(config_to_hello_cmd_auth)
{
config cfg;
cfg.clientname = "";
cfg.username = "foo";
cfg.password = "bar";
request req;
push_hello(cfg, req);
std::string_view const
expected = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n";
BOOST_CHECK_EQUAL(req.payload(), expected);
}
BOOST_AUTO_TEST_CASE(issue_210_empty_set)
{
try {