Skip to content

Commit 7373f36

Browse files
Add unit-tests for HttpServerConnection and HTTP message classes
1 parent 4782ea8 commit 7373f36

8 files changed

+1387
-0
lines changed

test/CMakeLists.txt

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,10 @@ set(base_test_SOURCES
8787
icinga-notification.cpp
8888
icinga-perfdata.cpp
8989
methods-pluginnotificationtask.cpp
90+
remote-certificate-fixture.cpp
9091
remote-configpackageutility.cpp
92+
remote-httpserverconnection.cpp
93+
remote-httpmessage.cpp
9194
remote-url.cpp
9295
${base_OBJS}
9396
$<TARGET_OBJECTS:config>
@@ -271,6 +274,33 @@ add_boost_test(base
271274
icinga_perfdata/parse_edgecases
272275
icinga_perfdata/empty_warn_crit_min_max
273276
methods_pluginnotificationtask/truncate_long_output
277+
remote_certs_fixture/prepare_directory
278+
remote_certs_fixture/cleanup_certs
279+
remote_httpmessage/request_parse
280+
remote_httpmessage/request_params
281+
remote_httpmessage/response_clear
282+
remote_httpmessage/response_flush_nothrow
283+
remote_httpmessage/response_flush_throw
284+
remote_httpmessage/response_write_empty
285+
remote_httpmessage/response_write_fixed
286+
remote_httpmessage/response_write_chunked
287+
remote_httpmessage/response_sendjsonbody
288+
remote_httpmessage/response_sendjsonerror
289+
remote_httpmessage/response_sendfile
290+
remote_httpserverconnection/expect_100_continue
291+
remote_httpserverconnection/bad_request
292+
remote_httpserverconnection/error_access_control
293+
remote_httpserverconnection/error_accept_header
294+
remote_httpserverconnection/authenticate_cn
295+
remote_httpserverconnection/authenticate_passwd
296+
remote_httpserverconnection/authenticate_error_wronguser
297+
remote_httpserverconnection/authenticate_error_wrongpasswd
298+
remote_httpserverconnection/reuse_connection
299+
remote_httpserverconnection/wg_abort
300+
remote_httpserverconnection/client_shutdown
301+
remote_httpserverconnection/handler_throw_error
302+
remote_httpserverconnection/handler_throw_streaming
303+
remote_httpserverconnection/liveness_disconnect
274304
remote_configpackageutility/ValidateName
275305
remote_url/id_and_path
276306
remote_url/parameters
@@ -279,6 +309,46 @@ add_boost_test(base
279309
remote_url/illegal_legal_strings
280310
)
281311

312+
if(BUILD_TESTING)
313+
set_tests_properties(
314+
base-remote_httpmessage/request_parse
315+
base-remote_httpmessage/request_params
316+
base-remote_httpmessage/response_clear
317+
base-remote_httpmessage/response_flush_nothrow
318+
base-remote_httpmessage/response_flush_throw
319+
base-remote_httpmessage/response_write_empty
320+
base-remote_httpmessage/response_write_fixed
321+
base-remote_httpmessage/response_write_chunked
322+
base-remote_httpmessage/response_sendjsonbody
323+
base-remote_httpmessage/response_sendjsonerror
324+
base-remote_httpmessage/response_sendfile
325+
base-remote_httpserverconnection/expect_100_continue
326+
base-remote_httpserverconnection/bad_request
327+
base-remote_httpserverconnection/error_access_control
328+
base-remote_httpserverconnection/error_accept_header
329+
base-remote_httpserverconnection/authenticate_cn
330+
base-remote_httpserverconnection/authenticate_passwd
331+
base-remote_httpserverconnection/authenticate_error_wronguser
332+
base-remote_httpserverconnection/authenticate_error_wrongpasswd
333+
base-remote_httpserverconnection/reuse_connection
334+
base-remote_httpserverconnection/wg_abort
335+
base-remote_httpserverconnection/client_shutdown
336+
base-remote_httpserverconnection/handler_throw_error
337+
base-remote_httpserverconnection/handler_throw_streaming
338+
base-remote_httpserverconnection/liveness_disconnect
339+
PROPERTIES FIXTURES_REQUIRED ssl_certs)
340+
341+
set_tests_properties(
342+
base-remote_certs_fixture/prepare_directory
343+
PROPERTIES FIXTURES_SETUP ssl_certs
344+
)
345+
346+
set_tests_properties(
347+
base-remote_certs_fixture/cleanup_certs
348+
PROPERTIES FIXTURES_CLEANUP ssl_certs
349+
)
350+
endif()
351+
282352
if(ICINGA2_WITH_LIVESTATUS)
283353
set(livestatus_test_SOURCES
284354
icingaapplication-fixture.cpp
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
2+
3+
#ifndef CONFIGURATION_FIXTURE_H
4+
#define CONFIGURATION_FIXTURE_H
5+
6+
#include "base/configuration.hpp"
7+
#include <boost/filesystem.hpp>
8+
#include <BoostTestTargetConfig.h>
9+
10+
namespace icinga {
11+
12+
struct ConfigurationDataDirFixture
13+
{
14+
ConfigurationDataDirFixture()
15+
: m_DataDir(boost::filesystem::current_path() / "data"), m_PrevDataDir(Configuration::DataDir.GetData())
16+
{
17+
boost::filesystem::create_directories(m_DataDir);
18+
Configuration::DataDir = m_DataDir.string();
19+
}
20+
21+
~ConfigurationDataDirFixture()
22+
{
23+
boost::filesystem::remove_all(m_DataDir);
24+
Configuration::DataDir = m_PrevDataDir.string();
25+
}
26+
27+
boost::filesystem::path m_DataDir;
28+
29+
private:
30+
boost::filesystem::path m_PrevDataDir;
31+
};
32+
33+
struct ConfigurationCacheDirFixture
34+
{
35+
ConfigurationCacheDirFixture()
36+
: m_CacheDir(boost::filesystem::current_path() / "cache"), m_PrevCacheDir(Configuration::CacheDir.GetData())
37+
{
38+
boost::filesystem::create_directories(m_CacheDir);
39+
Configuration::CacheDir = m_CacheDir.string();
40+
}
41+
42+
~ConfigurationCacheDirFixture()
43+
{
44+
boost::filesystem::remove_all(m_CacheDir);
45+
Configuration::CacheDir = m_PrevCacheDir.string();
46+
}
47+
48+
boost::filesystem::path m_CacheDir;
49+
50+
private:
51+
boost::filesystem::path m_PrevCacheDir;
52+
};
53+
54+
} // namespace icinga
55+
56+
#endif // CONFIGURATION_FIXTURE_H

test/base-testloggerfixture.hpp

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
2+
3+
#ifndef TEST_LOGGER_FIXTURE_H
4+
#define TEST_LOGGER_FIXTURE_H
5+
6+
#include <BoostTestTargetConfig.h>
7+
#include "base/logger.hpp"
8+
#include <boost/range/algorithm.hpp>
9+
#include <boost/regex.hpp>
10+
#include <boost/test/test_tools.hpp>
11+
#include <future>
12+
13+
namespace icinga {
14+
15+
class TestLogger : public Logger
16+
{
17+
public:
18+
DECLARE_PTR_TYPEDEFS(TestLogger);
19+
20+
struct Expect
21+
{
22+
std::string pattern;
23+
std::promise<bool> prom;
24+
};
25+
26+
auto ExpectLogPattern(const std::string& pattern,
27+
const std::chrono::milliseconds& timeout = std::chrono::seconds(0))
28+
{
29+
std::unique_lock lock(m_Mutex);
30+
for (const auto& logEntry : m_LogEntries) {
31+
if (boost::regex_match(logEntry.Message.GetData(), boost::regex(pattern))) {
32+
return boost::test_tools::assertion_result{true};
33+
}
34+
}
35+
36+
if (timeout == std::chrono::seconds(0)) {
37+
return boost::test_tools::assertion_result{false};
38+
}
39+
40+
auto expect = std::make_shared<Expect>(Expect{pattern, std::promise<bool>()});
41+
m_Expects.emplace_back(expect);
42+
lock.unlock();
43+
44+
auto future = expect->prom.get_future();
45+
auto status = future.wait_for(timeout);
46+
boost::test_tools::assertion_result ret{status == std::future_status::ready && future.get()};
47+
ret.message() << "Pattern \"" << pattern << "\" in log within " << timeout.count() << "ms";
48+
49+
lock.lock();
50+
m_Expects.erase(boost::range::remove(m_Expects, expect), m_Expects.end());
51+
52+
return ret;
53+
}
54+
55+
private:
56+
void ProcessLogEntry(const LogEntry& entry) override
57+
{
58+
std::unique_lock lock(m_Mutex);
59+
m_LogEntries.push_back(entry);
60+
61+
auto it = boost::range::remove_if(m_Expects, [&entry](const std::shared_ptr<Expect>& expect) {
62+
if (boost::regex_match(entry.Message.GetData(), boost::regex(expect->pattern))) {
63+
expect->prom.set_value(true);
64+
return true;
65+
}
66+
return false;
67+
});
68+
m_Expects.erase(it, m_Expects.end());
69+
}
70+
71+
void Flush() override {}
72+
73+
std::mutex m_Mutex;
74+
std::vector<std::shared_ptr<Expect>> m_Expects;
75+
std::vector<LogEntry> m_LogEntries;
76+
};
77+
78+
/**
79+
* A fixture to capture log entries and assert their presence in tests.
80+
*
81+
* Currently, this only supports checking existing entries and waiting for new ones
82+
* using ExpectLogPattern(), but more functionality can easily be added in the future,
83+
* like only asserting on past log messages, only waiting for new ones, asserting log
84+
* entry metadata (severity etc.) and so on.
85+
*/
86+
struct TestLoggerFixture
87+
{
88+
TestLoggerFixture()
89+
{
90+
testLogger->SetSeverity(testLogger->SeverityToString(LogDebug));
91+
testLogger->Activate(true);
92+
testLogger->SetActive(true);
93+
}
94+
95+
~TestLoggerFixture()
96+
{
97+
testLogger->SetActive(false);
98+
testLogger->Deactivate(true);
99+
}
100+
101+
/**
102+
* Asserts the presence of a log entry that matches the given regex pattern.
103+
*
104+
* First, the existing log entries are searched for the pattern. If the pattern isn't found,
105+
* until the timeout is reached, the function will wait if a new log message is added that
106+
* matches the pattern.
107+
*
108+
* A boost assertion result object is returned, that evaluates to bool, but contains an
109+
* error message that is printed by the testing framework when the assert failed.
110+
*
111+
* @param pattern The regex pattern the log message needs to match
112+
* @param timeout The maximum amount of time to wait for the log message to arrive
113+
*
114+
* @return A @c boost::test_tools::assertion_result object that can be used in BOOST_REQUIRE
115+
*/
116+
auto ExpectLogPattern(const std::string& pattern,
117+
const std::chrono::milliseconds& timeout = std::chrono::seconds(0))
118+
{
119+
return testLogger->ExpectLogPattern(pattern, timeout);
120+
}
121+
122+
TestLogger::Ptr testLogger = new TestLogger;
123+
};
124+
125+
} // namespace icinga
126+
127+
#endif // TEST_LOGGER_FIXTURE_H

test/base-tlsstream-fixture.hpp

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
2+
3+
#pragma once
4+
5+
#include "base/io-engine.hpp"
6+
#include "base/tlsstream.hpp"
7+
#include "test/remote-certificate-fixture.hpp"
8+
#include <BoostTestTargetConfig.h>
9+
#include <future>
10+
11+
namespace icinga {
12+
13+
/**
14+
* Creates a pair of TLS Streams on a random unused port.
15+
*/
16+
struct TlsStreamFixture : CertificateFixture
17+
{
18+
TlsStreamFixture()
19+
{
20+
using namespace boost::asio::ip;
21+
using handshake_type = boost::asio::ssl::stream_base::handshake_type;
22+
23+
auto serverCert = EnsureCertFor("server");
24+
auto clientCert = EnsureCertFor("client");
25+
26+
auto& io = IoEngine::Get().GetIoContext();
27+
28+
m_ClientSslContext = SetupSslContext(clientCert.crtFile, clientCert.keyFile, m_CaCrtFile.string(), "",
29+
DEFAULT_TLS_CIPHERS, DEFAULT_TLS_PROTOCOLMIN, DebugInfo());
30+
client = Shared<AsioTlsStream>::Make(io, *m_ClientSslContext);
31+
32+
m_ServerSslContext = SetupSslContext(serverCert.crtFile, serverCert.keyFile, m_CaCrtFile.string(), "",
33+
DEFAULT_TLS_CIPHERS, DEFAULT_TLS_PROTOCOLMIN, DebugInfo());
34+
server = Shared<AsioTlsStream>::Make(io, *m_ServerSslContext);
35+
36+
std::promise<void> p;
37+
38+
tcp::acceptor acceptor{io, tcp::endpoint{address_v4::loopback(), 0}};
39+
acceptor.listen();
40+
acceptor.async_accept(server->lowest_layer(), [&](const boost::system::error_code& ec) {
41+
if (ec) {
42+
BOOST_TEST_MESSAGE("Server Accept Error: " + ec.message());
43+
p.set_exception(std::make_exception_ptr(boost::system::system_error{ec}));
44+
return;
45+
}
46+
server->next_layer().async_handshake(handshake_type::server, [&](const boost::system::error_code& ec) {
47+
if (ec) {
48+
BOOST_TEST_MESSAGE("Server Handshake Error: " + ec.message());
49+
p.set_exception(std::make_exception_ptr(boost::system::system_error{ec}));
50+
return;
51+
}
52+
53+
if (!server->next_layer().IsVerifyOK()) {
54+
p.set_exception(std::make_exception_ptr(std::runtime_error{"Verify failed on server-side."}));
55+
}
56+
57+
p.set_value();
58+
});
59+
});
60+
61+
auto f = p.get_future();
62+
boost::system::error_code ec;
63+
if (client->lowest_layer().connect(acceptor.local_endpoint(), ec)) {
64+
BOOST_TEST_MESSAGE("Client Connect error: " + ec.message());
65+
f.get();
66+
BOOST_THROW_EXCEPTION(boost::system::system_error{ec});
67+
}
68+
69+
if (client->next_layer().handshake(handshake_type::client, ec)) {
70+
BOOST_TEST_MESSAGE("Client Handshake error: " + ec.message());
71+
f.get();
72+
BOOST_THROW_EXCEPTION(boost::system::system_error{ec});
73+
}
74+
75+
if (!client->next_layer().IsVerifyOK()) {
76+
f.get();
77+
BOOST_THROW_EXCEPTION(std::runtime_error{"Verify failed on client-side."});
78+
}
79+
80+
f.get();
81+
}
82+
83+
auto Shutdown(const Shared<AsioTlsStream>::Ptr& stream, std::optional<boost::asio::yield_context> yc = {})
84+
{
85+
boost::system::error_code ec;
86+
if (yc) {
87+
stream->next_layer().async_shutdown((*yc)[ec]);
88+
} else {
89+
stream->next_layer().shutdown(ec);
90+
}
91+
#if BOOST_VERSION < 107000
92+
/* On boost versions < 1.70, the end-of-file condition was propagated as an error,
93+
* even in case of a successful shutdown. This is information can be found in the
94+
* changelog for the boost Asio 1.14.0 / Boost 1.70 release.
95+
*/
96+
if (ec == boost::asio::error::eof) {
97+
BOOST_TEST_MESSAGE("Shutdown completed successfully with 'boost::asio::error::eof'.");
98+
return boost::test_tools::assertion_result{true};
99+
}
100+
#endif
101+
boost::test_tools::assertion_result ret{!ec};
102+
ret.message() << "Error: " << ec.message();
103+
return ret;
104+
}
105+
106+
Shared<AsioTlsStream>::Ptr client;
107+
Shared<AsioTlsStream>::Ptr server;
108+
109+
private:
110+
Shared<boost::asio::ssl::context>::Ptr m_ClientSslContext;
111+
Shared<boost::asio::ssl::context>::Ptr m_ServerSslContext;
112+
};
113+
114+
} // namespace icinga

0 commit comments

Comments
 (0)