mirror of
https://github.com/Icinga/icinga2.git
synced 2025-07-23 05:34:48 +02:00
Add unit-tests for HttpServerConnection and HTTP message classes
This commit is contained in:
parent
87b89a7175
commit
151d8a8969
@ -87,7 +87,10 @@ set(base_test_SOURCES
|
||||
icinga-notification.cpp
|
||||
icinga-perfdata.cpp
|
||||
methods-pluginnotificationtask.cpp
|
||||
remote-sslcert-fixture.cpp
|
||||
remote-configpackageutility.cpp
|
||||
remote-httpserverconnection.cpp
|
||||
remote-httpmessage.cpp
|
||||
remote-url.cpp
|
||||
${base_OBJS}
|
||||
$<TARGET_OBJECTS:config>
|
||||
@ -271,6 +274,30 @@ add_boost_test(base
|
||||
icinga_perfdata/parse_edgecases
|
||||
icinga_perfdata/empty_warn_crit_min_max
|
||||
methods_pluginnotificationtask/truncate_long_output
|
||||
remote_certs/setup_certs
|
||||
remote_certs/cleanup_certs
|
||||
remote_httpmessage/request_parse
|
||||
remote_httpmessage/request_params
|
||||
remote_httpmessage/response_flush_nothrow
|
||||
remote_httpmessage/response_body_reader
|
||||
remote_httpmessage/response_write_empty
|
||||
remote_httpmessage/response_write_fixed
|
||||
remote_httpmessage/response_write_chunked
|
||||
remote_httpmessage/response_sendjsonbody
|
||||
remote_httpmessage/response_sendjsonerror
|
||||
remote_httpserverconnection/expect_100_continue
|
||||
remote_httpserverconnection/bad_request
|
||||
remote_httpserverconnection/error_access_control
|
||||
remote_httpserverconnection/error_accept_header
|
||||
remote_httpserverconnection/error_authenticate_cn
|
||||
remote_httpserverconnection/error_authenticate_passwd
|
||||
remote_httpserverconnection/error_authenticate_wronguser
|
||||
remote_httpserverconnection/error_authenticate_wrongpasswd
|
||||
remote_httpserverconnection/reuse_connection
|
||||
remote_httpserverconnection/wg_abort
|
||||
remote_httpserverconnection/client_shutdown
|
||||
remote_httpserverconnection/handler_throw
|
||||
remote_httpserverconnection/liveness_disconnect
|
||||
remote_configpackageutility/ValidateName
|
||||
remote_url/id_and_path
|
||||
remote_url/parameters
|
||||
@ -279,6 +306,43 @@ add_boost_test(base
|
||||
remote_url/illegal_legal_strings
|
||||
)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
set_tests_properties(
|
||||
base-remote_httpmessage/request_parse
|
||||
base-remote_httpmessage/request_params
|
||||
base-remote_httpmessage/response_flush_nothrow
|
||||
base-remote_httpmessage/response_body_reader
|
||||
base-remote_httpmessage/response_write_empty
|
||||
base-remote_httpmessage/response_write_fixed
|
||||
base-remote_httpmessage/response_write_chunked
|
||||
base-remote_httpmessage/response_sendjsonbody
|
||||
base-remote_httpmessage/response_sendjsonerror
|
||||
base-remote_httpserverconnection/expect_100_continue
|
||||
base-remote_httpserverconnection/bad_request
|
||||
base-remote_httpserverconnection/error_access_control
|
||||
base-remote_httpserverconnection/error_accept_header
|
||||
base-remote_httpserverconnection/error_authenticate_cn
|
||||
base-remote_httpserverconnection/error_authenticate_passwd
|
||||
base-remote_httpserverconnection/error_authenticate_wronguser
|
||||
base-remote_httpserverconnection/error_authenticate_wrongpasswd
|
||||
base-remote_httpserverconnection/reuse_connection
|
||||
base-remote_httpserverconnection/wg_abort
|
||||
base-remote_httpserverconnection/client_shutdown
|
||||
base-remote_httpserverconnection/handler_throw
|
||||
base-remote_httpserverconnection/liveness_disconnect
|
||||
PROPERTIES FIXTURES_REQUIRED ssl_certs)
|
||||
|
||||
set_tests_properties(
|
||||
base-remote_certs/setup_certs
|
||||
PROPERTIES FIXTURES_SETUP ssl_certs
|
||||
)
|
||||
|
||||
set_tests_properties(
|
||||
base-remote_certs/cleanup_certs
|
||||
PROPERTIES FIXTURES_CLEANUP ssl_certs
|
||||
)
|
||||
endif()
|
||||
|
||||
if(ICINGA2_WITH_LIVESTATUS)
|
||||
set(livestatus_test_SOURCES
|
||||
icingaapplication-fixture.cpp
|
||||
|
50
test/base-configuration-fixture.hpp
Normal file
50
test/base-configuration-fixture.hpp
Normal file
@ -0,0 +1,50 @@
|
||||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#ifndef CONFIGURATION_FIXTURE_H
|
||||
#define CONFIGURATION_FIXTURE_H
|
||||
|
||||
#include "base/configuration.hpp"
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <BoostTestTargetConfig.h>
|
||||
|
||||
namespace icinga {
|
||||
|
||||
struct ConfigurationDataDirFixture
|
||||
{
|
||||
ConfigurationDataDirFixture() : m_DataDir(boost::filesystem::current_path() / "data"), m_PrevDataDir(Configuration::DataDir.GetData())
|
||||
{
|
||||
Configuration::DataDir = m_DataDir.string();
|
||||
boost::filesystem::create_directories(m_DataDir);
|
||||
}
|
||||
|
||||
~ConfigurationDataDirFixture()
|
||||
{
|
||||
boost::filesystem::remove_all(m_DataDir);
|
||||
Configuration::DataDir = m_PrevDataDir.string();
|
||||
}
|
||||
|
||||
boost::filesystem::path m_DataDir;
|
||||
boost::filesystem::path m_PrevDataDir;
|
||||
};
|
||||
|
||||
struct ConfigurationCacheDirFixture
|
||||
{
|
||||
ConfigurationCacheDirFixture() : m_CacheDir(boost::filesystem::current_path() / "data"), m_PrevCacheDir(Configuration::CacheDir.GetData())
|
||||
{
|
||||
Configuration::CacheDir = m_CacheDir.string();
|
||||
boost::filesystem::create_directories(m_CacheDir);
|
||||
}
|
||||
|
||||
~ConfigurationCacheDirFixture()
|
||||
{
|
||||
boost::filesystem::remove_all(m_CacheDir);
|
||||
Configuration::CacheDir = m_PrevCacheDir.string();
|
||||
}
|
||||
|
||||
boost::filesystem::path m_CacheDir;
|
||||
boost::filesystem::path m_PrevCacheDir;
|
||||
};
|
||||
|
||||
} // namespace icinga
|
||||
|
||||
#endif // CONFIGURATION_FIXTURE_H
|
91
test/base-testloggerfixture.hpp
Normal file
91
test/base-testloggerfixture.hpp
Normal file
@ -0,0 +1,91 @@
|
||||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#ifndef TEST_LOGGER_FIXTURE_H
|
||||
#define TEST_LOGGER_FIXTURE_H
|
||||
|
||||
#include "base/logger.hpp"
|
||||
#include <boost/test/test_tools.hpp>
|
||||
#include <boost/regex.hpp>
|
||||
#include <future>
|
||||
#include <BoostTestTargetConfig.h>
|
||||
|
||||
namespace icinga {
|
||||
|
||||
class TestLogger : public Logger
|
||||
{
|
||||
public:
|
||||
DECLARE_PTR_TYPEDEFS(TestLogger);
|
||||
|
||||
struct Expect
|
||||
{
|
||||
std::string pattern;
|
||||
std::promise<bool> prom;
|
||||
std::shared_future<bool> crutch;
|
||||
};
|
||||
|
||||
auto ExpectLogPattern(const std::string& pattern, const std::chrono::milliseconds& timeout = std::chrono::seconds(0))
|
||||
{
|
||||
std::unique_lock lock(m_Mutex);
|
||||
for (const auto & logEntry : m_LogEntries) {
|
||||
if (boost::regex_match(logEntry.Message.GetData(), boost::regex(pattern))) {
|
||||
return boost::test_tools::assertion_result{true};
|
||||
}
|
||||
}
|
||||
|
||||
if (timeout == std::chrono::seconds(0)) {
|
||||
return boost::test_tools::assertion_result{false};
|
||||
}
|
||||
|
||||
auto prom = std::promise<bool>();
|
||||
auto fut = prom.get_future().share();
|
||||
m_Expects.emplace_back(Expect{pattern, std::move(prom), fut});
|
||||
lock.unlock();
|
||||
|
||||
auto status = fut.wait_for(timeout);
|
||||
boost::test_tools::assertion_result ret{status == std::future_status::ready && fut.get()};
|
||||
ret.message() << "Pattern \"" << pattern << "\" in log within " << timeout.count() << "ms";
|
||||
return ret;
|
||||
}
|
||||
|
||||
private:
|
||||
void ProcessLogEntry(const LogEntry& entry) override
|
||||
{
|
||||
std::unique_lock lock(m_Mutex);
|
||||
m_LogEntries.push_back(entry);
|
||||
|
||||
m_Expects.erase(std::remove_if(m_Expects.begin(), m_Expects.end(), [&entry](Expect& expect) {
|
||||
if (boost::regex_match(entry.Message.GetData(), boost::regex(expect.pattern))) {
|
||||
expect.prom.set_value(true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}), m_Expects.end());
|
||||
}
|
||||
|
||||
void Flush() override {}
|
||||
|
||||
std::mutex m_Mutex;
|
||||
std::vector<Expect> m_Expects;
|
||||
std::vector<LogEntry> m_LogEntries;
|
||||
};
|
||||
|
||||
struct TestLoggerFixture
|
||||
{
|
||||
TestLoggerFixture()
|
||||
{
|
||||
testLogger->SetSeverity(testLogger->SeverityToString(LogDebug));
|
||||
testLogger->Activate(true);
|
||||
testLogger->SetActive(true);
|
||||
}
|
||||
|
||||
auto ExpectLogPattern(const std::string& pattern, const std::chrono::milliseconds& timeout = std::chrono::seconds(0))
|
||||
{
|
||||
return testLogger->ExpectLogPattern(pattern, timeout);
|
||||
}
|
||||
|
||||
TestLogger::Ptr testLogger = new TestLogger;
|
||||
};
|
||||
|
||||
} // namespace icinga
|
||||
|
||||
#endif // TEST_LOGGER_FIXTURE_H
|
105
test/base-tlsstream-fixture.hpp
Normal file
105
test/base-tlsstream-fixture.hpp
Normal file
@ -0,0 +1,105 @@
|
||||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#ifndef TLSSTREAM_FIXTURE_H
|
||||
#define TLSSTREAM_FIXTURE_H
|
||||
|
||||
#include "remote-sslcert-fixture.hpp"
|
||||
#include "base/tlsstream.hpp"
|
||||
#include "base/io-engine.hpp"
|
||||
#include <future>
|
||||
#include <BoostTestTargetConfig.h>
|
||||
|
||||
namespace icinga {
|
||||
|
||||
/**
|
||||
* Creates a pair of TLS Streams on a random unused port.
|
||||
*/
|
||||
struct TlsStreamFixture: CertificateFixture
|
||||
{
|
||||
TlsStreamFixture()
|
||||
{
|
||||
using namespace boost::asio::ip;
|
||||
using handshake_type = boost::asio::ssl::stream_base::handshake_type;
|
||||
|
||||
auto serverCert = EnsureCertFor("server");
|
||||
auto clientCert = EnsureCertFor("client");
|
||||
|
||||
auto& serverIoContext = IoEngine::Get().GetIoContext();
|
||||
|
||||
clientSslContext = SetupSslContext(clientCert.crtFile, clientCert.keyFile,
|
||||
m_CaCrtFile.string(), "", DEFAULT_TLS_CIPHERS, DEFAULT_TLS_PROTOCOLMIN, DebugInfo());
|
||||
client = Shared<AsioTlsStream>::Make(IoEngine::Get().GetIoContext(), *clientSslContext);
|
||||
|
||||
serverSslContext = SetupSslContext(serverCert.crtFile, serverCert.keyFile,
|
||||
m_CaCrtFile.string(), "", DEFAULT_TLS_CIPHERS, DEFAULT_TLS_PROTOCOLMIN, DebugInfo());
|
||||
server = Shared<AsioTlsStream>::Make(serverIoContext, *serverSslContext);
|
||||
|
||||
std::promise<void> p;
|
||||
|
||||
tcp::acceptor acceptor{serverIoContext, tcp::endpoint{address_v4::loopback(), 0}};
|
||||
acceptor.listen();
|
||||
acceptor.async_accept(server->lowest_layer(), [&, this](const boost::system::error_code& ec) {
|
||||
if (ec) {
|
||||
BOOST_TEST_MESSAGE("Server Accept Error: " + ec.message());
|
||||
return;
|
||||
}
|
||||
server->next_layer().async_handshake(handshake_type::server, [&, this](const boost::system::error_code& ec) {
|
||||
if (ec) {
|
||||
BOOST_TEST_MESSAGE("Server Handshake Error: " + ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
p.set_value();
|
||||
});
|
||||
});
|
||||
|
||||
boost::system::error_code ec;
|
||||
if (client->lowest_layer().connect(acceptor.local_endpoint(), ec)) {
|
||||
BOOST_TEST_MESSAGE("Client Connect error: " + ec.message());
|
||||
}
|
||||
|
||||
if (client->next_layer().handshake(handshake_type::client, ec)) {
|
||||
BOOST_TEST_MESSAGE("Client Handshake error: " + ec.message());
|
||||
}
|
||||
if (!client->next_layer().IsVerifyOK()) {
|
||||
BOOST_TEST_MESSAGE("Verify failed for connection");
|
||||
throw;
|
||||
}
|
||||
|
||||
p.get_future().wait();
|
||||
}
|
||||
|
||||
auto Shutdown(const Shared<AsioTlsStream>::Ptr& stream, std::optional<boost::asio::yield_context> yc = {})
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
if (yc) {
|
||||
stream->next_layer().async_shutdown((*yc)[ec]);
|
||||
}
|
||||
else{
|
||||
stream->next_layer().shutdown(ec);
|
||||
}
|
||||
#if BOOST_VERSION < 107000
|
||||
/* On boost versions < 1.70, the end-of-file condition was propagated as an error,
|
||||
* even in case of a successful shutdown. This is information can be found in the
|
||||
* changelog for the boost Asio 1.14.0 / Boost 1.70 release.
|
||||
*/
|
||||
if (ec == boost::asio::error::eof) {
|
||||
BOOST_TEST_MESSAGE("Shutdown completed successfully with 'boost::asio::error::eof'.");
|
||||
return boost::test_tools::assertion_result{true};
|
||||
}
|
||||
#endif
|
||||
boost::test_tools::assertion_result ret{!ec};
|
||||
ret.message() << "Error: " << ec.message();
|
||||
return ret;
|
||||
}
|
||||
|
||||
Shared<boost::asio::ssl::context>::Ptr clientSslContext;
|
||||
Shared<AsioTlsStream>::Ptr client;
|
||||
|
||||
Shared<boost::asio::ssl::context>::Ptr serverSslContext;
|
||||
Shared<AsioTlsStream>::Ptr server;
|
||||
};
|
||||
|
||||
} // namespace icinga
|
||||
|
||||
#endif // TLSSTREAM_FIXTURE_H
|
@ -26,6 +26,8 @@ void IcingaApplicationFixture::InitIcingaApplication()
|
||||
|
||||
IcingaApplicationFixture::~IcingaApplicationFixture()
|
||||
{
|
||||
BOOST_TEST_MESSAGE("Uninitializing Application...");
|
||||
Application::UninitializeBase();
|
||||
IcingaApplication::GetInstance().reset();
|
||||
}
|
||||
|
||||
|
258
test/remote-httpmessage.cpp
Normal file
258
test/remote-httpmessage.cpp
Normal file
@ -0,0 +1,258 @@
|
||||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#include "test/base-tlsstream-fixture.hpp"
|
||||
#include "remote/httpmessage.hpp"
|
||||
#include "base/base64.hpp"
|
||||
#include "base/json.hpp"
|
||||
#include <BoostTestTargetConfig.h>
|
||||
|
||||
using namespace icinga;
|
||||
using namespace boost::beast;
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(remote_httpmessage, TlsStreamFixture)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(request_parse)
|
||||
{
|
||||
http::request<boost::beast::http::string_body> requestOut;
|
||||
requestOut.method(http::verb::get);
|
||||
requestOut.target("https://localhost:5665/v1/test");
|
||||
requestOut.set(http::field::authorization, "Basic " + Base64::Encode("invalid:invalid"));
|
||||
requestOut.set(http::field::accept, "application/json");
|
||||
requestOut.set(http::field::connection, "close");
|
||||
requestOut.content_length(0);
|
||||
|
||||
auto & io = IoEngine::Get();
|
||||
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||
boost::beast::flat_buffer buf;
|
||||
HttpRequest request(server);
|
||||
server->async_fill(yc);
|
||||
BOOST_REQUIRE_NO_THROW(request.ParseHeader(buf, yc));
|
||||
|
||||
for (auto & field : requestOut.base()) {
|
||||
BOOST_REQUIRE(request.count(field.name()));
|
||||
}
|
||||
|
||||
request.ParseBody(buf, yc);
|
||||
|
||||
BOOST_REQUIRE(Shutdown(server, yc));
|
||||
});
|
||||
|
||||
http::write(*client, requestOut);
|
||||
client->flush();
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(request_params)
|
||||
{
|
||||
HttpRequest request(client);
|
||||
request.body() = JsonEncode(new Dictionary{{"test1", false}, {"test2", true}});
|
||||
request.target("https://localhost:1234/v1/test?test1=1&test3=0&test3=1");
|
||||
request.DecodeParams();
|
||||
|
||||
BOOST_REQUIRE(!request.Params()->Get("test2").IsObjectType<Array>());
|
||||
BOOST_REQUIRE(request.Params()->Get("test2").IsBoolean());
|
||||
BOOST_REQUIRE(request.Params()->Get("test2").ToBool());
|
||||
BOOST_REQUIRE(request.Params()->Get("test1").IsObjectType<Array>());
|
||||
BOOST_REQUIRE(request.GetLastParameter("test1"));
|
||||
BOOST_REQUIRE(request.Params()->Get("test3").IsObjectType<Array>());
|
||||
BOOST_REQUIRE(request.GetLastParameter("test3"));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(response_body_reader)
|
||||
{
|
||||
auto & io = IoEngine::Get();
|
||||
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||
HttpResponse response(server);
|
||||
response.result(http::status::ok);
|
||||
response.StartStreaming();
|
||||
|
||||
SerializableBody<boost::beast::multi_buffer>::reader rd{response.base(), response.body()};
|
||||
boost::system::error_code ec;
|
||||
rd.init(3, ec);
|
||||
BOOST_REQUIRE(!ec);
|
||||
|
||||
boost::asio::const_buffer seq1("test1", 5);
|
||||
rd.put(seq1, ec);
|
||||
BOOST_REQUIRE(!ec);
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
|
||||
boost::asio::const_buffer seq2("test2", 5);
|
||||
rd.put(seq2, ec);
|
||||
BOOST_REQUIRE(!ec);
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
|
||||
rd.finish(ec);
|
||||
BOOST_REQUIRE(!ec);
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
|
||||
BOOST_REQUIRE(Shutdown(server, yc));
|
||||
});
|
||||
|
||||
http::response_parser<http::string_body> parser;
|
||||
flat_buffer buf;
|
||||
http::read(*client, buf, parser);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().chunked(), true);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().body(), "test1test2");
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(response_flush_nothrow)
|
||||
{
|
||||
auto& io = IoEngine::Get();
|
||||
std::promise<void> done;
|
||||
|
||||
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||
HttpResponse response(server);
|
||||
response.result(http::status::ok);
|
||||
|
||||
server->lowest_layer().close();
|
||||
|
||||
boost::beast::error_code ec;
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc[ec]));
|
||||
BOOST_REQUIRE_EQUAL(ec, boost::system::errc::bad_file_descriptor);
|
||||
|
||||
done.set_value();
|
||||
});
|
||||
|
||||
auto status = done.get_future().wait_for(std::chrono::seconds(1));
|
||||
|
||||
BOOST_REQUIRE(status == std::future_status::ready);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(response_write_empty)
|
||||
{
|
||||
auto & io = IoEngine::Get();
|
||||
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||
HttpResponse response(server);
|
||||
response.result(http::status::ok);
|
||||
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
|
||||
BOOST_REQUIRE(Shutdown(server, yc));
|
||||
});
|
||||
|
||||
http::response_parser<http::string_body> parser;
|
||||
flat_buffer buf;
|
||||
http::read(*client, buf, parser);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().chunked(), false);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().body(), "");
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(response_write_fixed)
|
||||
{
|
||||
auto & io = IoEngine::Get();
|
||||
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||
HttpResponse response(server);
|
||||
response.result(http::status::ok);
|
||||
response.body() << "test";
|
||||
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
|
||||
BOOST_REQUIRE(Shutdown(server, yc));
|
||||
});
|
||||
|
||||
http::response_parser<http::string_body> parser;
|
||||
flat_buffer buf;
|
||||
http::read(*client, buf, parser);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().chunked(), false);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().body(), "test");
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(response_write_chunked)
|
||||
{
|
||||
auto & io = IoEngine::Get();
|
||||
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||
HttpResponse response(server);
|
||||
response.result(http::status::ok);
|
||||
|
||||
response.StartStreaming();
|
||||
response.body() << "test" << 1;
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
|
||||
response.body() << "test" << 2;
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
|
||||
response.body() << "test" << 3;
|
||||
response.body().Finish();
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
|
||||
BOOST_REQUIRE(Shutdown(server, yc));
|
||||
});
|
||||
|
||||
http::response_parser<http::string_body> parser;
|
||||
flat_buffer buf;
|
||||
http::read(*client, buf, parser);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().chunked(), true);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().body(), "test1test2test3");
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(response_sendjsonbody)
|
||||
{
|
||||
auto & io = IoEngine::Get();
|
||||
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||
HttpResponse response(server);
|
||||
response.result(http::status::ok);
|
||||
|
||||
response.SendJsonBody(new Dictionary{{"test", 1}});
|
||||
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
BOOST_REQUIRE(Shutdown(server, yc));
|
||||
});
|
||||
|
||||
http::response_parser<http::string_body> parser;
|
||||
flat_buffer buf;
|
||||
http::read(*client, buf, parser);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().chunked(), false);
|
||||
Dictionary::Ptr body = JsonDecode(parser.get().body());
|
||||
BOOST_REQUIRE_EQUAL(body->Get("test"), 1);
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(response_sendjsonerror)
|
||||
{
|
||||
auto & io = IoEngine::Get();
|
||||
io.SpawnCoroutine(io.GetIoContext(), [&, this](boost::asio::yield_context yc) {
|
||||
HttpResponse response(server);
|
||||
|
||||
// This has to be overwritten in SendJsonError.
|
||||
response.result(http::status::ok);
|
||||
|
||||
response.SendJsonError(404, "Not found.");
|
||||
|
||||
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
||||
BOOST_REQUIRE(Shutdown(server, yc));
|
||||
});
|
||||
|
||||
http::response_parser<http::string_body> parser;
|
||||
flat_buffer buf;
|
||||
http::read(*client, buf, parser);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::not_found);
|
||||
BOOST_REQUIRE_EQUAL(parser.get().chunked(), false);
|
||||
Dictionary::Ptr body = JsonDecode(parser.get().body());
|
||||
BOOST_REQUIRE_EQUAL(body->Get("error"), 404);
|
||||
BOOST_REQUIRE_EQUAL(body->Get("status"), "Not found.");
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
482
test/remote-httpserverconnection.cpp
Normal file
482
test/remote-httpserverconnection.cpp
Normal file
@ -0,0 +1,482 @@
|
||||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#include "base-tlsstream-fixture.hpp"
|
||||
#include "icingaapplication-fixture.hpp"
|
||||
#include "base-testloggerfixture.hpp"
|
||||
#include "base/base64.hpp"
|
||||
#include "base/json.hpp"
|
||||
#include "remote/httphandler.hpp"
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/container/flat_set.hpp>
|
||||
#include <boost/version.hpp>
|
||||
#include <BoostTestTargetConfig.h>
|
||||
|
||||
using namespace icinga;
|
||||
using namespace boost::beast;
|
||||
// using namespace boost::unit_test;
|
||||
using namespace boost::unit_test_framework;
|
||||
|
||||
struct HttpServerConnectionFixture : TlsStreamFixture,
|
||||
IcingaApplicationFixture,
|
||||
ConfigurationCacheDirFixture,
|
||||
TestLoggerFixture
|
||||
{
|
||||
HttpServerConnection::Ptr conn;
|
||||
StoppableWaitGroup::Ptr wg;
|
||||
|
||||
HttpServerConnectionFixture()
|
||||
: wg(new StoppableWaitGroup)
|
||||
{
|
||||
Logger::SetConsoleLogSeverity(icinga::LogDebug);
|
||||
}
|
||||
|
||||
static void CreateApiListener()
|
||||
{
|
||||
ScriptGlobal::Set("NodeName", "server");
|
||||
ApiListener::Ptr listener = new ApiListener;
|
||||
listener->OnConfigLoaded();
|
||||
listener->SetAccessControlAllowOrigin(new Array{"127.0.0.1"});
|
||||
}
|
||||
|
||||
static void CreateTestUsers()
|
||||
{
|
||||
ApiUser::Ptr user = new ApiUser;
|
||||
user->SetName("client");
|
||||
user->SetClientCN("client");
|
||||
user->SetPermissions(new Array{"*"});
|
||||
user->Register();
|
||||
|
||||
user = new ApiUser;
|
||||
user->SetName("test");
|
||||
user->SetPassword("test");
|
||||
user->SetPermissions(new Array{"*"});
|
||||
user->Register();
|
||||
}
|
||||
|
||||
void SetupHttpServerConnection(bool authenticated)
|
||||
{
|
||||
String identity = authenticated ? "client" : "invalid";
|
||||
conn = new HttpServerConnection(wg, identity, authenticated, server);
|
||||
conn->Start();
|
||||
}
|
||||
|
||||
template <class Rep, class Period>
|
||||
bool AssertServerDisconnected(const std::chrono::duration<Rep, Period>& timeout)
|
||||
{
|
||||
auto iterations = timeout / std::chrono::milliseconds(50);
|
||||
for(std::size_t i=0; i<iterations && !conn->Disconnected(); i++){
|
||||
Utility::Sleep(std::chrono::duration<double>(timeout).count()/iterations);
|
||||
}
|
||||
return conn->Disconnected();
|
||||
}
|
||||
};
|
||||
|
||||
class UnitTestHandler final: public HttpHandler
|
||||
{
|
||||
|
||||
bool HandleRequest(const WaitGroup::Ptr& waitGroup, const HttpRequest& request, HttpResponse& response,
|
||||
boost::asio::yield_context& yc) override
|
||||
{
|
||||
response.result(boost::beast::http::status::ok);
|
||||
|
||||
if (request.GetLastParameter("stream")) {
|
||||
response.StartStreaming();
|
||||
response.Flush(yc);
|
||||
for (;;) {
|
||||
response.body() << "test";
|
||||
response.Flush(yc);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (request.GetLastParameter("throw")) {
|
||||
response.StartStreaming();
|
||||
response.body() << "test";
|
||||
response.Flush(yc);
|
||||
throw std::runtime_error{"The front fell off"};
|
||||
}
|
||||
|
||||
response.body() << "test";
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
REGISTER_URLHANDLER("/v1/test", UnitTestHandler);
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(remote_httpserverconnection, HttpServerConnectionFixture)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(expect_100_continue)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(true);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.version(11);
|
||||
request.target("https://localhost:5665/v1/test");
|
||||
request.set(http::field::expect, "100-continue");
|
||||
request.set(http::field::accept, "application/json");
|
||||
request.set(http::field::connection, "close");
|
||||
request.content_length(0);
|
||||
http::request_serializer<http::string_body> sr(request);
|
||||
http::write_header(*client, sr);
|
||||
client->flush();
|
||||
|
||||
flat_buffer buf;
|
||||
http::response<http::string_body> response;
|
||||
http::read(*client, buf, response);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||
BOOST_REQUIRE_EQUAL(response.result(), http::status::continue_);
|
||||
|
||||
http::write(*client, sr);
|
||||
client->flush();
|
||||
|
||||
http::read(*client, buf, response);
|
||||
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(response.body(), "test");
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(bad_request)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(true);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.version(12);
|
||||
request.target("https://localhost:5665/v1/test");
|
||||
request.set(http::field::accept, "application/json");
|
||||
request.set(http::field::connection, "close");
|
||||
request.content_length(0);
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
flat_buffer buf;
|
||||
http::response<http::string_body> response;
|
||||
http::read(*client, buf, response);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(response.result(), http::status::bad_request);
|
||||
BOOST_REQUIRE_NE(response.body().find("<h1>Bad Request</h1>"), std::string::npos);
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(error_access_control)
|
||||
{
|
||||
CreateTestUsers();
|
||||
CreateApiListener();
|
||||
SetupHttpServerConnection(true);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::options);
|
||||
request.target("https://localhost:5665/v1/test");
|
||||
request.set(http::field::origin, "127.0.0.1");
|
||||
request.set(http::field::access_control_request_method, "GET");
|
||||
request.set(http::field::connection, "close");
|
||||
request.content_length(0);
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
flat_buffer buf;
|
||||
http::response<http::string_body> response;
|
||||
http::read(*client, buf, response);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(response.body(), "Preflight OK");
|
||||
|
||||
boost::container::flat_set<std::string> sv;
|
||||
boost::container::flat_set options{"GET", "POST", "PUT", "DELETE", "PUSH"};
|
||||
|
||||
BOOST_REQUIRE_NE(response[http::field::access_control_allow_methods], "");
|
||||
BOOST_REQUIRE_NE(response[http::field::access_control_allow_headers], "");
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(error_accept_header)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(true);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::post);
|
||||
request.target("https://localhost:5665/v1/test");
|
||||
request.set(http::field::accept, "text/html");
|
||||
request.set(http::field::connection, "close");
|
||||
request.content_length(0);
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
flat_buffer buf;
|
||||
http::response<http::string_body> response;
|
||||
http::read(*client, buf, response);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||
BOOST_REQUIRE_EQUAL(response.result(), http::status::bad_request);
|
||||
BOOST_REQUIRE_EQUAL(response.body(), "<h1>Accept header is missing or not set to 'application/json'.</h1>\r\n");
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(error_authenticate_cn)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(true);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("https://localhost:5665/v1/test");
|
||||
request.set(http::field::accept, "application/json");
|
||||
request.set(http::field::connection, "close");
|
||||
request.content_length(0);
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
flat_buffer buf;
|
||||
http::response<http::string_body> response;
|
||||
http::read(*client, buf, response);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(error_authenticate_passwd)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(false);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("https://localhost:5665/v1/test");
|
||||
request.set(http::field::authorization, "Basic " + Base64::Encode("test:test"));
|
||||
request.set(http::field::accept, "application/json");
|
||||
request.set(http::field::connection, "close");
|
||||
request.content_length(0);
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
flat_buffer buf;
|
||||
http::response<http::string_body> response;
|
||||
http::read(*client, buf, response);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(error_authenticate_wronguser)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(false);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("https://localhost:5665/v1/test");
|
||||
request.set(http::field::authorization, "Basic " + Base64::Encode("invalid:invalid"));
|
||||
request.set(http::field::accept, "application/json");
|
||||
request.set(http::field::connection, "close");
|
||||
request.content_length(0);
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
flat_buffer buf;
|
||||
http::response<http::string_body> response;
|
||||
http::read(*client, buf, response);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||
BOOST_REQUIRE_EQUAL(response.result(), http::status::unauthorized);
|
||||
Dictionary::Ptr body = JsonDecode(response.body());
|
||||
BOOST_REQUIRE(body);
|
||||
BOOST_REQUIRE_EQUAL(body->Get("error"), 401);
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(error_authenticate_wrongpasswd)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(false);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("https://localhost:5665/v1/test");
|
||||
request.set(http::field::authorization, "Basic " + Base64::Encode("test:invalid"));
|
||||
request.set(http::field::accept, "application/json");
|
||||
request.set(http::field::connection, "close");
|
||||
request.content_length(0);
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
flat_buffer buf;
|
||||
http::response<http::string_body> response;
|
||||
http::read(*client, buf, response);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||
BOOST_REQUIRE_EQUAL(response.result(), http::status::unauthorized);
|
||||
Dictionary::Ptr body = JsonDecode(response.body());
|
||||
BOOST_REQUIRE(body);
|
||||
BOOST_REQUIRE_EQUAL(body->Get("error"), 401);
|
||||
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(reuse_connection)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(true);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("https://localhost:5665/v1/test");
|
||||
request.set(http::field::accept, "application/json");
|
||||
request.keep_alive(true);
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
flat_buffer buf;
|
||||
http::response<http::string_body> response;
|
||||
http::read(*client, buf, response);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(response.body(), "test");
|
||||
|
||||
request.keep_alive(false);
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
response.body() = "";
|
||||
http::read(*client, buf, response);
|
||||
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(response.body(), "test");
|
||||
|
||||
BOOST_REQUIRE(AssertServerDisconnected(std::chrono::seconds(5)));
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
BOOST_REQUIRE(ExpectLogPattern("HTTP client disconnected .*", std::chrono::seconds(5)));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(wg_abort)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(true);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("https://localhost:5665/v1/test");
|
||||
request.set(http::field::accept, "application/json");
|
||||
request.keep_alive(true);
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
wg->Join();
|
||||
|
||||
flat_buffer buf;
|
||||
http::response<http::string_body> response;
|
||||
http::read(*client, buf, response);
|
||||
|
||||
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
||||
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
||||
BOOST_REQUIRE_EQUAL(response.body(), "test");
|
||||
|
||||
BOOST_REQUIRE(AssertServerDisconnected(std::chrono::seconds(5)));
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
BOOST_REQUIRE(ExpectLogPattern("HTTP client disconnected .*", std::chrono::seconds(5)));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(client_shutdown)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(true);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("https://localhost:5665/v1/test");
|
||||
request.set(http::field::accept, "application/json");
|
||||
request.keep_alive(true);
|
||||
|
||||
/* Instruct the test HttpHandler defined above to stream an endless response.
|
||||
*/
|
||||
request.body() = JsonEncode(new Dictionary{{"stream", true}});
|
||||
request.prepare_payload();
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
/* Unlike the other test cases we don't require success here, because with the request
|
||||
* above, UnitTestHandler simulates a HttpHandler that is constantly writing.
|
||||
* That will cause the shutdown to fail on the client-side with "application data after
|
||||
* close notify", but the important part is that HttpServerConnection actually closes
|
||||
* the connection on its own side, which we check with the BOOST_REQUIRE() below.
|
||||
*/
|
||||
BOOST_WARN(Shutdown(client));
|
||||
|
||||
BOOST_REQUIRE(AssertServerDisconnected(std::chrono::seconds(5)));
|
||||
BOOST_REQUIRE(ExpectLogPattern("HTTP client disconnected.*", std::chrono::seconds(5)));
|
||||
BOOST_REQUIRE(ExpectLogPattern("Detected shutdown from client: .*"));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(handler_throw)
|
||||
{
|
||||
CreateTestUsers();
|
||||
SetupHttpServerConnection(true);
|
||||
|
||||
http::request<boost::beast::http::string_body> request;
|
||||
request.method(http::verb::get);
|
||||
request.target("https://localhost:5665/v1/test");
|
||||
request.set(http::field::accept, "application/json");
|
||||
request.keep_alive(true);
|
||||
|
||||
/* Instruct the test TestHandler to throw an exception while streaming, which the
|
||||
* HttpHandler base should propagate up to HttpServerConnection.
|
||||
* The correct response is to shutdown the connection instead of trying to send a JSON
|
||||
* error message.
|
||||
*/
|
||||
request.body() = JsonEncode(new Dictionary{{"throw", true}});
|
||||
request.prepare_payload();
|
||||
http::write(*client, request);
|
||||
client->flush();
|
||||
|
||||
flat_buffer buf;
|
||||
http::response_parser<http::string_body> parser;
|
||||
boost::system::error_code ec;
|
||||
http::read(*client, buf, parser, ec);
|
||||
|
||||
/* Since the handler threw in the middle of sending the message we shouldn't be able
|
||||
* to read a complete message here.
|
||||
* Somehow boost::beast error codes can not be tested with BOOST_REQUIRE_EQUAL so we
|
||||
* compare the string instead.
|
||||
*/
|
||||
BOOST_REQUIRE_EQUAL(ec.message(), "partial message");
|
||||
|
||||
/* The body should only contain the single "test" the handler has written, without any
|
||||
* attempts made to additionally write some json error message.
|
||||
*/
|
||||
BOOST_REQUIRE_EQUAL(parser.get().body(), "test");
|
||||
|
||||
/* We then expect the server to initiate a shutdown, which we then complete below.
|
||||
*/
|
||||
BOOST_REQUIRE(AssertServerDisconnected(std::chrono::seconds(5)));
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
BOOST_REQUIRE(ExpectLogPattern("HTTP client disconnected.*", std::chrono::seconds(5)));
|
||||
BOOST_REQUIRE(ExpectLogPattern("Exception while processing HTTP request from .+?: The front fell off"));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(liveness_disconnect)
|
||||
{
|
||||
SetupHttpServerConnection(false);
|
||||
|
||||
BOOST_REQUIRE(AssertServerDisconnected(std::chrono::seconds(11)));
|
||||
BOOST_REQUIRE(ExpectLogPattern("HTTP client disconnected .*"));
|
||||
BOOST_REQUIRE(Shutdown(client));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
26
test/remote-sslcert-fixture.cpp
Normal file
26
test/remote-sslcert-fixture.cpp
Normal file
@ -0,0 +1,26 @@
|
||||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#include "remote-sslcert-fixture.hpp"
|
||||
#include <BoostTestTargetConfig.h>
|
||||
|
||||
using namespace icinga;
|
||||
|
||||
const boost::filesystem::path CertificateFixture::m_PersistentCertsDir = boost::filesystem::current_path() / "persistent" / "certs";
|
||||
|
||||
BOOST_AUTO_TEST_SUITE(remote_certs)
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(setup_certs, ConfigurationDataDirFixture)
|
||||
{
|
||||
if (boost::filesystem::exists(CertificateFixture::m_PersistentCertsDir)) {
|
||||
boost::filesystem::remove_all(CertificateFixture::m_PersistentCertsDir);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(cleanup_certs, ConfigurationDataDirFixture)
|
||||
{
|
||||
if (boost::filesystem::exists(CertificateFixture::m_PersistentCertsDir)) {
|
||||
boost::filesystem::remove_all(CertificateFixture::m_PersistentCertsDir);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
71
test/remote-sslcert-fixture.hpp
Normal file
71
test/remote-sslcert-fixture.hpp
Normal file
@ -0,0 +1,71 @@
|
||||
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
||||
|
||||
#ifndef SSLCERT_FIXTURE_H
|
||||
#define SSLCERT_FIXTURE_H
|
||||
|
||||
#include "remote/apilistener.hpp"
|
||||
#include "remote/pkiutility.hpp"
|
||||
#include "test/base-configuration-fixture.hpp"
|
||||
#include <BoostTestTargetConfig.h>
|
||||
|
||||
namespace icinga{
|
||||
|
||||
struct CertificateFixture: ConfigurationDataDirFixture
|
||||
{
|
||||
CertificateFixture()
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
m_CaDir = ApiListener::GetCaDir();
|
||||
m_CertsDir = ApiListener::GetCertsDir();
|
||||
m_CaCrtFile = m_CertsDir / "ca.crt";
|
||||
|
||||
fs::create_directories(m_PersistentCertsDir / "ca");
|
||||
fs::create_directories(m_PersistentCertsDir / "certs");
|
||||
|
||||
if (fs::exists(m_DataDir / "ca")) {
|
||||
fs::remove(m_DataDir / "ca");
|
||||
}
|
||||
if (fs::exists(m_DataDir / "certs")) {
|
||||
fs::remove(m_DataDir / "certs");
|
||||
}
|
||||
|
||||
fs::create_directory_symlink(m_PersistentCertsDir / "certs", m_DataDir / "certs");
|
||||
fs::create_directory_symlink(m_PersistentCertsDir / "ca", m_DataDir / "ca");
|
||||
|
||||
if (!fs::exists(m_CaCrtFile)) {
|
||||
PkiUtility::NewCa();
|
||||
fs::copy_file(m_CaDir / "ca.crt", m_CaCrtFile);
|
||||
}
|
||||
}
|
||||
|
||||
auto EnsureCertFor(const std::string& name)
|
||||
{
|
||||
struct
|
||||
{
|
||||
String crtFile;
|
||||
String keyFile;
|
||||
String csrFile;
|
||||
} cert;
|
||||
|
||||
cert.crtFile = (m_CertsDir/(name + ".crt")).string();
|
||||
cert.keyFile = (m_CertsDir/(name + ".key")).string();
|
||||
cert.csrFile = (m_CertsDir/(name + ".csr")).string();
|
||||
|
||||
if (!Utility::PathExists(cert.crtFile)) {
|
||||
PkiUtility::NewCert(name, cert.keyFile, cert.csrFile, cert.crtFile);
|
||||
PkiUtility::SignCsr(cert.csrFile, cert.crtFile);
|
||||
}
|
||||
|
||||
return cert;
|
||||
}
|
||||
|
||||
boost::filesystem::path m_CaDir;
|
||||
boost::filesystem::path m_CertsDir;
|
||||
boost::filesystem::path m_CaCrtFile;
|
||||
static const boost::filesystem::path m_PersistentCertsDir;
|
||||
};
|
||||
|
||||
} // namespace icinga
|
||||
|
||||
#endif // SSLCERT_FIXTURE_H
|
Loading…
x
Reference in New Issue
Block a user