mirror of
https://github.com/Icinga/icinga2.git
synced 2025-09-21 16:57:58 +02:00
This adds a global fixture that can parse an additional argument to the test executables (`--generate_ctest_config`). When run by CMake during build, this generates a CTest script containing all the tests and their properties. An additional decorator, that defines CTest properties for a test case or suite that will be added to the tests during config generation. This version needs no hacks, no huge CMake scripts, just a bit of additional C++ code that iterates over all test-cases and collects the information CTest needs. One caveat is still that this does not work with cross-compilation, which probably isn't an issue to begin with, but there are also ways to fix that if necessary.
357 lines
10 KiB
C++
357 lines
10 KiB
C++
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
|
|
|
|
#include <BoostTestTargetConfig.h>
|
|
#include "base/base64.hpp"
|
|
#include "base/json.hpp"
|
|
#include "remote/httpmessage.hpp"
|
|
#include "remote/httputility.hpp"
|
|
#include "test/base-tlsstream-fixture.hpp"
|
|
#include "test/test-ctest.hpp"
|
|
#include <fstream>
|
|
#include <utility>
|
|
|
|
using namespace icinga;
|
|
using namespace boost::beast;
|
|
|
|
static std::future<void> SpawnSynchronizedCoroutine(std::function<void(boost::asio::yield_context)> fn)
|
|
{
|
|
auto promise = std::make_unique<std::promise<void>>();
|
|
auto future = promise->get_future();
|
|
auto& io = IoEngine::Get().GetIoContext();
|
|
IoEngine::SpawnCoroutine(io, [promise = std::move(promise), fn = std::move(fn)](boost::asio::yield_context yc) {
|
|
try {
|
|
fn(std::move(yc));
|
|
} catch (const std::exception&) {
|
|
promise->set_exception(std::current_exception());
|
|
return;
|
|
}
|
|
promise->set_value();
|
|
});
|
|
return future;
|
|
}
|
|
|
|
// clang-format off
|
|
BOOST_FIXTURE_TEST_SUITE(remote_httpmessage, TlsStreamFixture,
|
|
*CTestProperties("FIXTURES_REQUIRED ssl_certs")
|
|
*boost::unit_test::label("http"))
|
|
// clang-format on
|
|
|
|
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.body() = "test";
|
|
requestOut.prepare_payload();
|
|
|
|
auto future = SpawnSynchronizedCoroutine([this, &requestOut](boost::asio::yield_context yc) {
|
|
boost::beast::flat_buffer buf;
|
|
HttpRequest request(server);
|
|
BOOST_REQUIRE_NO_THROW(request.ParseHeader(buf, yc));
|
|
|
|
for (const auto& field : requestOut.base()) {
|
|
BOOST_REQUIRE(request.count(field.name()));
|
|
}
|
|
|
|
BOOST_REQUIRE_NO_THROW(request.ParseBody(buf, yc));
|
|
BOOST_REQUIRE_EQUAL(request.body(), "test");
|
|
|
|
Shutdown(server, yc);
|
|
});
|
|
|
|
http::write(*client, requestOut);
|
|
client->flush();
|
|
|
|
Shutdown(client);
|
|
future.get();
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(request_params)
|
|
{
|
|
HttpRequest request(client);
|
|
// clang-format off
|
|
request.body() = JsonEncode(
|
|
new Dictionary{
|
|
{"bool-in-json", true},
|
|
{"bool-in-url-and-json", true},
|
|
{"string-in-json", "json-value"},
|
|
{"string-in-url-and-json", "json-value"}
|
|
});
|
|
request.target("https://localhost:1234/v1/test?"
|
|
"bool-in-url-and-json=0&"
|
|
"bool-in-url=1&"
|
|
"string-in-url-and-json=url-value&"
|
|
"string-only-in-url=url-value"
|
|
);
|
|
// clang-format on
|
|
|
|
// Test pointer being valid after decode
|
|
request.DecodeParams();
|
|
auto params = request.Params();
|
|
BOOST_REQUIRE(params);
|
|
|
|
// Test JSON-only params being parsed as their correct type
|
|
BOOST_REQUIRE(params->Get("bool-in-json").IsBoolean());
|
|
BOOST_REQUIRE(params->Get("string-in-json").IsString());
|
|
BOOST_REQUIRE(params->Get("bool-in-url-and-json").IsObjectType<Array>());
|
|
BOOST_REQUIRE(params->Get("string-in-url-and-json").IsObjectType<Array>());
|
|
|
|
// Test 0/1 string values from URL evaluate to true and false
|
|
// These currently get implicitly converted to double and then to bool, but this is an
|
|
// implementation we don't need to test for here.
|
|
BOOST_REQUIRE_EQUAL(HttpUtility::GetLastParameter(params, "bool-in-url-and-json"), "0");
|
|
BOOST_REQUIRE(!HttpUtility::GetLastParameter(params, "bool-in-url-and-json"));
|
|
BOOST_REQUIRE_EQUAL(HttpUtility::GetLastParameter(params, "bool-in-url"), "1");
|
|
BOOST_REQUIRE(HttpUtility::GetLastParameter(params, "bool-in-url"));
|
|
|
|
// Test non-existing parameters evaluate to false
|
|
BOOST_REQUIRE(HttpUtility::GetLastParameter(params, "does-not-exist").IsEmpty());
|
|
BOOST_REQUIRE(!HttpUtility::GetLastParameter(params, "does-not-exist"));
|
|
|
|
// Test precedence of URL params over JSON params
|
|
BOOST_REQUIRE_EQUAL(HttpUtility::GetLastParameter(params, "string-in-json"), "json-value");
|
|
BOOST_REQUIRE_EQUAL(HttpUtility::GetLastParameter(params, "string-in-url-and-json"), "url-value");
|
|
BOOST_REQUIRE_EQUAL(HttpUtility::GetLastParameter(params, "string-only-in-url"), "url-value");
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(response_clear)
|
|
{
|
|
HttpResponse response(server);
|
|
response.result(http::status::bad_request);
|
|
response.version(10);
|
|
response.set(http::field::content_type, "text/html");
|
|
response.body() << "test";
|
|
|
|
response.Clear();
|
|
|
|
BOOST_REQUIRE(response[http::field::content_type].empty());
|
|
BOOST_REQUIRE_EQUAL(response.result(), http::status::ok);
|
|
BOOST_REQUIRE_EQUAL(response.version(), 11);
|
|
BOOST_REQUIRE_EQUAL(response.body().Size(), 0);
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(response_flush_nothrow)
|
|
{
|
|
auto future = SpawnSynchronizedCoroutine([this](const 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);
|
|
});
|
|
|
|
auto status = future.wait_for(std::chrono::seconds(1));
|
|
BOOST_REQUIRE(status == std::future_status::ready);
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(response_flush_throw)
|
|
{
|
|
auto future = SpawnSynchronizedCoroutine([this](const boost::asio::yield_context& yc) {
|
|
HttpResponse response(server);
|
|
response.result(http::status::ok);
|
|
|
|
server->lowest_layer().close();
|
|
|
|
BOOST_REQUIRE_EXCEPTION(response.Flush(yc), std::exception, [](const std::exception& ex) {
|
|
auto se = dynamic_cast<const boost::system::system_error*>(&ex);
|
|
return se && se->code() == boost::system::errc::bad_file_descriptor;
|
|
});
|
|
});
|
|
|
|
auto status = future.wait_for(std::chrono::seconds(1));
|
|
BOOST_REQUIRE(status == std::future_status::ready);
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(response_write_empty)
|
|
{
|
|
auto future = SpawnSynchronizedCoroutine([this](boost::asio::yield_context yc) {
|
|
HttpResponse response(server);
|
|
response.result(http::status::ok);
|
|
|
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
|
|
|
Shutdown(server, yc);
|
|
});
|
|
|
|
http::response_parser<http::string_body> parser;
|
|
flat_buffer buf;
|
|
boost::system::error_code ec;
|
|
http::read(*client, buf, parser, ec);
|
|
|
|
Shutdown(client);
|
|
|
|
future.get();
|
|
|
|
BOOST_REQUIRE(!ec);
|
|
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
|
BOOST_REQUIRE_EQUAL(parser.get().chunked(), false);
|
|
BOOST_REQUIRE_EQUAL(parser.get().body(), "");
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(response_write_fixed)
|
|
{
|
|
auto future = SpawnSynchronizedCoroutine([this](boost::asio::yield_context yc) {
|
|
HttpResponse response(server);
|
|
response.result(http::status::ok);
|
|
response.body() << "test";
|
|
|
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
|
|
|
Shutdown(server, yc);
|
|
});
|
|
|
|
http::response_parser<http::string_body> parser;
|
|
flat_buffer buf;
|
|
boost::system::error_code ec;
|
|
http::read(*client, buf, parser, ec);
|
|
|
|
Shutdown(client);
|
|
|
|
future.get();
|
|
|
|
BOOST_REQUIRE(!ec);
|
|
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
|
BOOST_REQUIRE_EQUAL(parser.get().chunked(), false);
|
|
BOOST_REQUIRE_EQUAL(parser.get().body(), "test");
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(response_write_chunked)
|
|
{
|
|
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
|
auto future = SpawnSynchronizedCoroutine([this](boost::asio::yield_context yc) {
|
|
HttpResponse response(server);
|
|
response.result(http::status::ok);
|
|
|
|
response.StartStreaming();
|
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
|
BOOST_REQUIRE(response.HasSerializationStarted());
|
|
|
|
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));
|
|
|
|
Shutdown(server, yc);
|
|
});
|
|
|
|
http::response_parser<http::string_body> parser;
|
|
flat_buffer buf;
|
|
boost::system::error_code ec;
|
|
http::read(*client, buf, parser, ec);
|
|
|
|
Shutdown(client);
|
|
|
|
future.get();
|
|
|
|
BOOST_REQUIRE(!ec);
|
|
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
|
BOOST_REQUIRE_EQUAL(parser.get().chunked(), true);
|
|
BOOST_REQUIRE_EQUAL(parser.get().body(), "test1test2test3");
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(response_sendjsonbody)
|
|
{
|
|
auto future = SpawnSynchronizedCoroutine([this](boost::asio::yield_context yc) {
|
|
HttpResponse response(server);
|
|
response.result(http::status::ok);
|
|
|
|
HttpUtility::SendJsonBody(response, nullptr, new Dictionary{{"test", 1}});
|
|
|
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
|
|
|
Shutdown(server, yc);
|
|
});
|
|
|
|
http::response_parser<http::string_body> parser;
|
|
flat_buffer buf;
|
|
boost::system::error_code ec;
|
|
http::read(*client, buf, parser, ec);
|
|
|
|
Shutdown(client);
|
|
|
|
future.get();
|
|
|
|
BOOST_REQUIRE(!ec);
|
|
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_AUTO_TEST_CASE(response_sendjsonerror)
|
|
{
|
|
auto future = SpawnSynchronizedCoroutine([this](boost::asio::yield_context yc) {
|
|
HttpResponse response(server);
|
|
|
|
// This has to be overwritten in SendJsonError.
|
|
response.result(http::status::ok);
|
|
|
|
HttpUtility::SendJsonError(response, nullptr, 404, "Not found.");
|
|
|
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
|
|
|
Shutdown(server, yc);
|
|
});
|
|
|
|
http::response_parser<http::string_body> parser;
|
|
flat_buffer buf;
|
|
boost::system::error_code ec;
|
|
http::read(*client, buf, parser, ec);
|
|
|
|
Shutdown(client);
|
|
|
|
future.get();
|
|
|
|
BOOST_REQUIRE(!ec);
|
|
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_AUTO_TEST_CASE(response_sendfile)
|
|
{
|
|
auto future = SpawnSynchronizedCoroutine([this](boost::asio::yield_context yc) {
|
|
HttpResponse response(server);
|
|
|
|
response.result(http::status::ok);
|
|
BOOST_REQUIRE_NO_THROW(response.SendFile(m_CaCrtFile.string(), yc));
|
|
BOOST_REQUIRE_NO_THROW(response.Flush(yc));
|
|
|
|
Shutdown(server, yc);
|
|
});
|
|
|
|
http::response_parser<http::string_body> parser;
|
|
flat_buffer buf;
|
|
boost::system::error_code ec;
|
|
http::read(*client, buf, parser, ec);
|
|
|
|
Shutdown(client);
|
|
|
|
future.get();
|
|
|
|
BOOST_REQUIRE(!ec);
|
|
BOOST_REQUIRE_EQUAL(parser.get().result(), http::status::ok);
|
|
BOOST_REQUIRE_EQUAL(parser.get().chunked(), false);
|
|
|
|
std::ifstream fp(m_CaCrtFile.string(), std::ifstream::in | std::ifstream::binary);
|
|
fp.exceptions(std::ifstream::badbit);
|
|
std::stringstream ss;
|
|
ss << fp.rdbuf();
|
|
BOOST_REQUIRE_EQUAL(ss.str(), parser.get().body());
|
|
}
|
|
|
|
BOOST_AUTO_TEST_SUITE_END()
|