icinga2/test/perfdata-perfdatawriterconnection.cpp
2026-02-03 13:30:37 +01:00

227 lines
6.8 KiB
C++

/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
#include <utility>
#include "perfdata/perfdatawriterconnection.hpp"
#include "test/perfdata-perfdatatargetfixture.hpp"
#include "test/perfdata-perfdatawriterfixture.hpp"
#include "test/remote-certificate-fixture.hpp"
#include "test/test-ctest.hpp"
#include "test/test-thread.hpp"
#include "test/utils.hpp"
using namespace icinga;
class TlsPerfdataWriterFixture : public CertificateFixture, public PerfdataWriterTargetFixture
{
public:
TlsPerfdataWriterFixture() : PerfdataWriterTargetFixture(MakeContext())
{
auto pdwCert = EnsureCertFor("client");
m_PdwSslContext = SetupSslContext(
pdwCert.crtFile,
pdwCert.keyFile,
m_CaCrtFile.string(),
"",
DEFAULT_TLS_CIPHERS,
DEFAULT_TLS_PROTOCOLMIN,
DebugInfo()
);
m_Conn = new PerfdataWriterConnection{"127.0.0.1", std::to_string(GetPort()), m_PdwSslContext};
}
auto& GetConnection() { return *m_Conn; }
private:
Shared<boost::asio::ssl::context>::Ptr MakeContext()
{
auto testCert = EnsureCertFor("server");
return SetupSslContext(
testCert.crtFile,
testCert.keyFile,
m_CaCrtFile.string(),
"",
DEFAULT_TLS_CIPHERS,
DEFAULT_TLS_PROTOCOLMIN,
DebugInfo()
);
}
Shared<boost::asio::ssl::context>::Ptr m_PdwSslContext;
PerfdataWriterConnection::Ptr m_Conn;
};
BOOST_FIXTURE_TEST_SUITE(perfdatawriterconnection, TlsPerfdataWriterFixture,
*CTestProperties("FIXTURES_REQUIRED ssl_certs")
*boost::unit_test::label("perfdata"))
/* If there is no acceptor listening on the other side, connecting should fail.
*/
BOOST_AUTO_TEST_CASE(connection_refused)
{
CloseAcceptor();
TestThread mockTargetThread{[&]() { GetConnection().StartDisconnectTimeout(50ms); }};
BOOST_REQUIRE_EXCEPTION(
GetConnection().Send(boost::asio::const_buffer{"foobar", 7}),
boost::system::system_error,
[](const auto& ex) -> bool { return ex.code() == boost::asio::error::operation_aborted; }
);
REQUIRE_JOINS_WITHIN(mockTargetThread, 1s);
}
/* The PerfdataWriterConnection connects automatically when sending the first data.
* In case of http we also need to support disconnecting and reconnecting.
*/
BOOST_AUTO_TEST_CASE(ensure_connected)
{
TestThread mockTargetThread{[&]() {
Accept();
Handshake();
auto ret = GetDataUntil('\0');
BOOST_REQUIRE_EQUAL(ret, "foobar");
Shutdown();
/* Test a second cycle to make sure reusing the socket works.
*/
Accept();
Handshake();
ret = GetDataUntil('\0');
BOOST_REQUIRE_EQUAL(ret, "foobar");
Shutdown();
}};
GetConnection().Send(boost::asio::const_buffer{"foobar", 7});
GetConnection().Disconnect();
GetConnection().Send(boost::asio::const_buffer{"foobar", 7});
GetConnection().Disconnect();
REQUIRE_JOINS_WITHIN(mockTargetThread, 1s);
}
/* This tests a "manual" disconnect() while in the diconnection timeout, similar to what a
* perfdata writer does if the it manages to finish the WorkQueue before the timeout runs out.
*/
BOOST_AUTO_TEST_CASE(disconnect_during_timeout)
{
TestThread mockTargetThread{[&]() {
Accept();
Handshake();
auto ret = GetDataUntil('\0');
BOOST_REQUIRE_EQUAL(ret, "foobar");
Shutdown();
}};
BOOST_REQUIRE_NO_THROW(GetConnection().Send(boost::asio::const_buffer{"foobar", 7}));
GetConnection().StartDisconnectTimeout(50ms);
BOOST_REQUIRE_NO_THROW(GetConnection().Disconnect());
REQUIRE_JOINS_WITHIN(mockTargetThread, 1s);
}
/* While the grace-period given through StartDisconnectTimeout is active, data can still be sent,
* assuming we had already connected to the server.
*/
BOOST_AUTO_TEST_CASE(finish_during_timeout)
{
TestThread mockTargetThread{[&]() {
Accept();
Handshake();
auto ret = GetDataUntil('\0');
BOOST_REQUIRE_EQUAL(ret, "foobar");
ret = GetDataUntil('\0');
BOOST_REQUIRE_EQUAL(ret, "foobar");
Shutdown();
}};
GetConnection().Send(boost::asio::const_buffer{"foobar", 7});
GetConnection().StartDisconnectTimeout(50ms);
GetConnection().Send(boost::asio::const_buffer{"foobar", 7});
REQUIRE_JOINS_WITHIN(mockTargetThread, 1s);
}
/* Stops the connection before any attempt to connect anywhere has been made. This is needed to
* speed up quick successive reloads of the daemon.
*/
BOOST_AUTO_TEST_CASE(stop_immediately)
{
auto start = std::chrono::steady_clock::now();
GetConnection().StartDisconnectTimeout(50ms);
BOOST_REQUIRE_EXCEPTION(
GetConnection().Send(boost::asio::const_buffer{"foobar", 7}),
boost::system::system_error,
[](const auto& ex) -> bool { return ex.code() == boost::asio::error::operation_aborted; }
);
// Ensure that the Send() has actually returned before the timeout has elapsed.
BOOST_REQUIRE(std::chrono::steady_clock::now() - start < 40ms);
}
/* For the client, even a hanging server will accept the connection immediately, since it's done
* in the kernel. But in that case the TLS handshake will be stuck, so we need to verify that a
* handshake can be interrupted by StartDisconnectTimeout().
*/
BOOST_AUTO_TEST_CASE(stuck_in_handshake)
{
TestThread mockTargetThread{[&]() {
Accept();
GetConnection().StartDisconnectTimeout(50ms);
}};
BOOST_REQUIRE_EXCEPTION(
GetConnection().Send(boost::asio::const_buffer{"foobar", 7}),
boost::system::system_error,
[&](const auto& ex) { return ex.code() == boost::asio::error::operation_aborted; }
);
REQUIRE_JOINS_WITHIN(mockTargetThread, 1s);
}
/* When the disconnect timeout runs out while sending something to a slow or blocking server, we
* expect the send to be aborted immediately with an 'operation cancelled' exception, in order to
* not prolong the shutdown of a perfdata writer.
* No regular shutdown can be performed in this case, because the stream has been truncated. The
* server will need to handle this one on their own.
*/
BOOST_AUTO_TEST_CASE(stuck_sending)
{
TestThread mockTargetThread{[&]() {
Accept();
Handshake();
auto ret = GetDataUntil("#");
BOOST_REQUIRE_EQUAL(ret, "foobar");
/* This is necessary so ultry-slow machines (like the Github-workflow building the
* container image) don't interrupt the connection process with the successive 50ms
* disconnect timeout.
*/
auto start = std::chrono::steady_clock::now();
while (!GetConnection().IsConnected() && std::chrono::steady_clock::now() - start < 1s) {
std::this_thread::sleep_for(10ms);
}
GetConnection().StartDisconnectTimeout(1s);
}};
// Allocate a large string that will fill the buffers on both sides of the connection, in
// order to make Send() block.
auto randomData = GetRandomString("foobar#", 4UL * 1024 * 1024);
BOOST_REQUIRE_EXCEPTION(
GetConnection().Send(boost::asio::const_buffer{randomData.data(), randomData.size()}),
boost::system::system_error,
[&](const auto& ex) {
BOOST_TEST_INFO("Exception: " << ex.what());
return ex.code() == boost::asio::error::operation_aborted;
}
);
REQUIRE_JOINS_WITHIN(mockTargetThread, 1s);
}
BOOST_AUTO_TEST_SUITE_END()