Files
clio/tests/unit/web/ServerTests.cpp
Sergey Kuznetsov fe0bf736fb refactor: Use error code in make_address() calls (#3044)
Function `ip::make_address()` throws an exception on an invalid IP.
Refactor to a better error handling without exceptions.
2026-04-27 11:32:07 +01:00

823 lines
28 KiB
C++

#include "data/LedgerCacheInterface.hpp"
#include "util/AssignRandomPort.hpp"
#include "util/MockLedgerCache.hpp"
#include "util/MockPrometheus.hpp"
#include "util/TestHttpClient.hpp"
#include "util/TestWebSocketClient.hpp"
#include "util/TmpFile.hpp"
#include "util/config/Array.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigFileJson.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "util/prometheus/Label.hpp"
#include "util/prometheus/Prometheus.hpp"
#include "web/AdminVerificationStrategy.hpp"
#include "web/Server.hpp"
#include "web/dosguard/DOSGuard.hpp"
#include "web/dosguard/DOSGuardInterface.hpp"
#include "web/dosguard/IntervalSweepHandler.hpp"
#include "web/dosguard/Weights.hpp"
#include "web/dosguard/WhitelistHandler.hpp"
#include "web/interface/ConnectionBase.hpp"
#include <boost/asio/executor_work_guard.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/beast/core/error.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/websocket/error.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/value.hpp>
#include <boost/system/system_error.hpp>
#include <fmt/format.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <test_data/SslCert.hpp>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <tuple>
#include <utility>
#include <vector>
using namespace util;
using namespace util::config;
using namespace web::impl;
using namespace web;
static boost::json::value
generateJSONWithDynamicPort(std::string_view port)
{
return boost::json::parse(
fmt::format(
R"JSON({{
"server": {{
"ip": "0.0.0.0",
"port": {}
}},
"dos_guard": {{
"max_fetches": 100,
"sweep_interval": 1000,
"max_connections": 2,
"max_requests": 3,
"whitelist": ["127.0.0.1"]
}}
}})JSON",
port
)
);
}
static boost::json::value
generateJSONDataOverload(std::string_view port)
{
return boost::json::parse(
fmt::format(
R"JSON({{
"server": {{
"ip": "0.0.0.0",
"port": {}
}},
"dos_guard": {{
"max_fetches": 100,
"sweep_interval": 1000,
"max_connections": 2,
"max_requests": 1
}}
}})JSON",
port
)
);
}
inline static ClioConfigDefinition
getParseServerConfig(boost::json::value val)
{
ConfigFileJson const jsonVal{val.as_object()};
auto config = ClioConfigDefinition{
{"server.ip", ConfigValue{ConfigType::String}},
{"server.port", ConfigValue{ConfigType::Integer}},
{"server.admin_password", ConfigValue{ConfigType::String}.optional()},
{"server.local_admin", ConfigValue{ConfigType::Boolean}.optional()},
{"server.proxy.ips.[]", Array{ConfigValue{ConfigType::String}}},
{"server.proxy.tokens.[]", Array{ConfigValue{ConfigType::String}}},
{"server.ws_max_sending_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(1500)},
{"log.tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
{"dos_guard.max_fetches", ConfigValue{ConfigType::Integer}},
{"dos_guard.sweep_interval", ConfigValue{ConfigType::Integer}},
{"dos_guard.max_connections", ConfigValue{ConfigType::Integer}},
{"dos_guard.max_requests", ConfigValue{ConfigType::Integer}},
{"dos_guard.whitelist.[]", Array{ConfigValue{ConfigType::String}.optional()}},
{"ssl_key_file", ConfigValue{ConfigType::String}.optional()},
{"ssl_cert_file", ConfigValue{ConfigType::String}.optional()},
};
auto const errors = config.parse(jsonVal);
[&]() { ASSERT_FALSE(errors.has_value()); }();
return config;
};
struct WebServerTest : public virtual ::testing::Test {
~WebServerTest() override
{
work_.reset();
ctx.stop();
if (runner_->joinable())
runner_->join();
}
WebServerTest()
{
work_.emplace(boost::asio::make_work_guard(ctx)); // make sure ctx does not stop on its own
runner_.emplace([this] { ctx.run(); });
}
boost::json::value
addSslConfig(boost::json::value config) const
{
config.as_object()["ssl_key_file"] = sslKeyFile.path;
config.as_object()["ssl_cert_file"] = sslCertFile.path;
return config;
}
// this ctx is for dos timer
boost::asio::io_context ctxSync;
std::string const port = std::to_string(tests::util::generateFreePort());
ClioConfigDefinition cfg{getParseServerConfig(generateJSONWithDynamicPort(port))};
dosguard::WhitelistHandler whitelistHandler{dosguard::WhitelistHandler::create(cfg).value()};
dosguard::Weights dosguardWeights{1, {}};
dosguard::DOSGuard dosGuard{cfg, whitelistHandler, dosguardWeights};
dosguard::IntervalSweepHandler sweepHandler{cfg, ctxSync, dosGuard};
ClioConfigDefinition cfgOverload{getParseServerConfig(generateJSONDataOverload(port))};
dosguard::WhitelistHandler whitelistHandlerOverload{
dosguard::WhitelistHandler::create(cfgOverload).value()
};
dosguard::DOSGuard dosGuardOverload{cfgOverload, whitelistHandlerOverload, dosguardWeights};
dosguard::IntervalSweepHandler sweepHandlerOverload{cfgOverload, ctxSync, dosGuardOverload};
// this ctx is for http server
boost::asio::io_context ctx;
TmpFile sslCertFile{tests::sslCertFile()};
TmpFile sslKeyFile{tests::sslKeyFile()};
private:
std::optional<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>> work_;
std::optional<std::thread> runner_;
};
class EchoExecutor {
public:
void
operator()(std::string const& reqStr, std::shared_ptr<web::ConnectionBase> const& ws)
{
ws->send(std::string(reqStr), http::status::ok);
}
void
operator()(
boost::beast::error_code /* ec */,
std::shared_ptr<web::ConnectionBase> const& /* ws */
)
{
}
};
class ExceptionExecutor {
public:
void
operator()(std::string const& /* req */, std::shared_ptr<web::ConnectionBase> const& /* ws */)
{
throw std::runtime_error("MyError");
}
void
operator()(
boost::beast::error_code /* ec */,
std::shared_ptr<web::ConnectionBase> const& /* ws */
)
{
}
};
namespace {
template <class Executor>
std::shared_ptr<web::HttpServer<Executor>>
makeServerSync(
util::config::ClioConfigDefinition const& config,
boost::asio::io_context& ioc,
web::dosguard::DOSGuardInterface& dosGuard,
std::shared_ptr<Executor> const& handler,
std::reference_wrapper<data::LedgerCacheInterface const> cache
)
{
auto result = web::makeHttpServer(config, ioc, dosGuard, handler, cache);
[&]() { ASSERT_TRUE(result.has_value()); }();
return std::move(result).value();
}
} // namespace
TEST_F(WebServerTest, InvalidIpAddress)
{
auto jsonConfig = generateJSONWithDynamicPort(port);
jsonConfig.as_object()["server"].as_object()["ip"] = "not-an-ip";
auto cache = MockLedgerCache();
auto const e = std::make_shared<EchoExecutor>();
auto const result =
web::makeHttpServer(getParseServerConfig(jsonConfig), ctx, dosGuard, e, cache);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(result.error(), testing::HasSubstr("Invalid 'server.ip' config value"));
}
TEST_F(WebServerTest, Http)
{
auto cache = MockLedgerCache();
auto const e = std::make_shared<EchoExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
auto const [status, res] = HttpSyncClient::post("localhost", port, R"JSON({"Hello":1})JSON");
EXPECT_EQ(res, R"JSON({"Hello":1})JSON");
EXPECT_EQ(status, boost::beast::http::status::ok);
}
TEST_F(WebServerTest, Ws)
{
auto cache = MockLedgerCache();
auto e = std::make_shared<EchoExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
WebSocketSyncClient wsClient;
wsClient.connect("localhost", port);
auto const res = wsClient.syncPost(R"JSON({"Hello":1})JSON");
EXPECT_EQ(res, R"JSON({"Hello":1})JSON");
wsClient.disconnect();
}
TEST_F(WebServerTest, HttpInternalError)
{
auto cache = MockLedgerCache();
auto const e = std::make_shared<ExceptionExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
auto const [status, res] = HttpSyncClient::post("localhost", port, R"JSON({})JSON");
EXPECT_EQ(
res,
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})JSON"
);
EXPECT_EQ(status, boost::beast::http::status::internal_server_error);
}
TEST_F(WebServerTest, WsInternalError)
{
auto cache = MockLedgerCache();
auto e = std::make_shared<ExceptionExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
WebSocketSyncClient wsClient;
wsClient.connect("localhost", port);
auto const res = wsClient.syncPost(R"JSON({"id":"id1"})JSON");
wsClient.disconnect();
EXPECT_EQ(
res,
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":"id1","request":{"id":"id1"}})JSON"
);
}
TEST_F(WebServerTest, WsInternalErrorNotJson)
{
auto cache = MockLedgerCache();
auto e = std::make_shared<ExceptionExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
WebSocketSyncClient wsClient;
wsClient.connect("localhost", port);
auto const res = wsClient.syncPost("not json");
wsClient.disconnect();
EXPECT_EQ(
res,
R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","request":"not json"})JSON"
);
}
TEST_F(WebServerTest, IncompleteSslConfig)
{
auto const e = std::make_shared<EchoExecutor>();
auto jsonConfig = generateJSONWithDynamicPort(port);
jsonConfig.as_object()["ssl_key_file"] = sslKeyFile.path;
auto cache = MockLedgerCache();
auto const result =
web::makeHttpServer(getParseServerConfig(jsonConfig), ctx, dosGuard, e, cache);
EXPECT_FALSE(result.has_value());
}
TEST_F(WebServerTest, WrongSslConfig)
{
auto const e = std::make_shared<EchoExecutor>();
auto jsonConfig = generateJSONWithDynamicPort(port);
jsonConfig.as_object()["ssl_key_file"] = sslKeyFile.path;
jsonConfig.as_object()["ssl_cert_file"] = "wrong_path";
auto cache = MockLedgerCache();
auto const result =
web::makeHttpServer(getParseServerConfig(jsonConfig), ctx, dosGuard, e, cache);
EXPECT_FALSE(result.has_value());
}
TEST_F(WebServerTest, Https)
{
auto cache = MockLedgerCache();
auto const e = std::make_shared<EchoExecutor>();
cfg = getParseServerConfig(addSslConfig(generateJSONWithDynamicPort(port)));
auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
auto const res = HttpsSyncClient::syncPost("localhost", port, R"JSON({"Hello":1})JSON");
EXPECT_EQ(res, R"JSON({"Hello":1})JSON");
}
TEST_F(WebServerTest, Wss)
{
auto cache = MockLedgerCache();
auto e = std::make_shared<EchoExecutor>();
cfg = getParseServerConfig(addSslConfig(generateJSONWithDynamicPort(port)));
auto server = makeServerSync(cfg, ctx, dosGuard, e, cache);
WebServerSslSyncClient wsClient;
wsClient.connect("localhost", port);
auto const res = wsClient.syncPost(R"JSON({"Hello":1})JSON");
EXPECT_EQ(res, R"JSON({"Hello":1})JSON");
wsClient.disconnect();
}
TEST_F(WebServerTest, HttpPayloadOverload)
{
std::string const s100(100, 'a');
auto cache = MockLedgerCache();
auto const e = std::make_shared<EchoExecutor>();
auto server = makeServerSync(cfg, ctx, dosGuardOverload, e, cache);
auto const [status, res] =
HttpSyncClient::post("localhost", port, fmt::format(R"JSON({{"payload":"{}"}})JSON", s100));
EXPECT_EQ(
res,
R"JSON({"payload":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","warning":"load","warnings":[{"id":2003,"message":"You are about to be rate limited"}]})JSON"
);
EXPECT_EQ(status, boost::beast::http::status::ok);
}
TEST_F(WebServerTest, WsPayloadOverload)
{
std::string const s100(100, 'a');
auto cache = MockLedgerCache();
auto const e = std::make_shared<EchoExecutor>();
auto server = makeServerSync(cfg, ctx, dosGuardOverload, e, cache);
WebSocketSyncClient wsClient;
wsClient.connect("localhost", port);
auto const res = wsClient.syncPost(fmt::format(R"JSON({{"payload":"{}"}})JSON", s100));
wsClient.disconnect();
EXPECT_EQ(
res,
R"JSON({"payload":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","warning":"load","warnings":[{"id":2003,"message":"You are about to be rate limited"}]})JSON"
);
}
TEST_F(WebServerTest, WsTooManyConnection)
{
auto cache = MockLedgerCache();
auto const e = std::make_shared<EchoExecutor>();
auto server = makeServerSync(cfg, ctx, dosGuardOverload, e, cache);
// max connection is 2, exception should happen when the third connection is made
WebSocketSyncClient wsClient1;
wsClient1.connect("localhost", port);
WebSocketSyncClient wsClient2;
wsClient2.connect("localhost", port);
bool exceptionThrown = false;
try {
WebSocketSyncClient wsClient3;
wsClient3.connect("localhost", port);
} catch (boost::system::system_error const& ex) {
exceptionThrown = true;
EXPECT_EQ(ex.code(), boost::beast::websocket::error::upgrade_declined);
}
wsClient1.disconnect();
wsClient2.disconnect();
EXPECT_TRUE(exceptionThrown);
}
TEST_F(WebServerTest, HealthCheck)
{
auto cache = MockLedgerCache();
auto e = std::make_shared<ExceptionExecutor>(); // request handled before we get to executor
auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
auto const [status, res] = HttpSyncClient::get("localhost", port, "", "/health");
EXPECT_FALSE(res.empty());
EXPECT_EQ(status, boost::beast::http::status::ok);
}
TEST_F(WebServerTest, CacheStateCheckWithLoadedCache)
{
auto cache = MockLedgerCache();
EXPECT_CALL(cache, isFull()).WillRepeatedly(testing::Return(true));
auto e = std::make_shared<ExceptionExecutor>(); // request handled before we get to executor
auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
auto const [status, res] = HttpSyncClient::get("localhost", port, "", "/cache_state");
EXPECT_FALSE(res.empty());
EXPECT_EQ(status, boost::beast::http::status::ok);
}
TEST_F(WebServerTest, CacheStateCheckWithoutLoadedCache)
{
auto cache = MockLedgerCache();
EXPECT_CALL(cache, isFull()).WillRepeatedly(testing::Return(false));
auto e = std::make_shared<ExceptionExecutor>(); // request handled before we get to executor
auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
auto const [status, res] = HttpSyncClient::get("localhost", port, "", "/cache_state");
EXPECT_FALSE(res.empty());
EXPECT_EQ(status, boost::beast::http::status::service_unavailable);
}
TEST_F(WebServerTest, GetOtherThanHealthCheck)
{
auto cache = MockLedgerCache();
auto e = std::make_shared<ExceptionExecutor>(); // request handled before we get to executor
auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
auto const [status, res] = HttpSyncClient::get("localhost", port, "", "/");
EXPECT_FALSE(res.empty());
EXPECT_EQ(status, boost::beast::http::status::bad_request);
}
namespace {
std::string
jsonServerConfigWithAdminPassword(uint32_t const port)
{
return fmt::format(
R"JSON({{
"server": {{
"ip": "0.0.0.0",
"port": {},
"admin_password": "secret"
}}
}})JSON",
port
);
}
std::string
jsonServerConfigWithLocalAdmin(uint32_t const port)
{
return fmt::format(
R"JSON({{
"server": {{
"ip": "0.0.0.0",
"port": {},
"local_admin": true
}}
}})JSON",
port
);
}
std::string
jsonServerConfigWithBothAdminPasswordAndLocalAdminFalse(uint32_t const port)
{
return fmt::format(
R"JSON({{
"server": {{
"ip": "0.0.0.0",
"port": {},
"admin_password": "secret",
"local_admin": false
}}
}})JSON",
port
);
}
std::string
jsonServerConfigWithNoSpecifiedAdmin(uint32_t const port)
{
return fmt::format(
R"JSON({{
"server": {{
"ip": "0.0.0.0",
"port": {}
}}
}})JSON",
port
);
}
// get this value from online sha256 generator
constexpr auto kSECRET_SHA256 = "2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b";
} // namespace
class AdminCheckExecutor {
public:
void
operator()(std::string const& reqStr, std::shared_ptr<web::ConnectionBase> const& ws)
{
auto response = fmt::format("{} {}", reqStr, ws->isAdmin() ? "admin" : "user");
ws->send(std::move(response), http::status::ok);
}
void
operator()(
boost::beast::error_code /* ec */,
std::shared_ptr<web::ConnectionBase> const& /* ws */
)
{
}
};
struct WebServerAdminTestParams {
std::string config;
std::vector<WebHeader> headers;
std::string expectedResponse;
};
inline static ClioConfigDefinition
getParseAdminServerConfig(boost::json::value val)
{
ConfigFileJson const jsonVal{val.as_object()};
auto config = ClioConfigDefinition{
{"server.ip", ConfigValue{ConfigType::String}},
{"server.port", ConfigValue{ConfigType::Integer}},
{"server.admin_password", ConfigValue{ConfigType::String}.optional()},
{"server.local_admin", ConfigValue{ConfigType::Boolean}.optional()},
{"server.processing_policy", ConfigValue{ConfigType::String}.defaultValue("parallel")},
{"server.proxy.ips.[]", Array{ConfigValue{ConfigType::String}}},
{"server.proxy.tokens.[]", Array{ConfigValue{ConfigType::String}}},
{"server.parallel_requests_limit", ConfigValue{ConfigType::Integer}.optional()},
{"server.ws_max_sending_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(1500)},
{"ssl_cert_file", ConfigValue{ConfigType::String}.optional()},
{"ssl_key_file", ConfigValue{ConfigType::String}.optional()},
{"prometheus.enabled", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
{"prometheus.compress_reply", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
{"log.tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}
};
auto const errors = config.parse(jsonVal);
[&]() { ASSERT_FALSE(errors.has_value()); }();
return config;
};
class WebServerAdminTest : public WebServerTest,
public ::testing::WithParamInterface<WebServerAdminTestParams> {};
TEST_P(WebServerAdminTest, WsAdminCheck)
{
auto cache = MockLedgerCache();
auto e = std::make_shared<AdminCheckExecutor>();
ClioConfigDefinition const serverConfig{
getParseAdminServerConfig(boost::json::parse(GetParam().config))
};
auto server = makeServerSync(serverConfig, ctx, dosGuardOverload, e, cache);
WebSocketSyncClient wsClient;
uint32_t const webServerPort = serverConfig.get<uint32_t>("server.port");
wsClient.connect("localhost", std::to_string(webServerPort), GetParam().headers);
std::string const request = "Why hello";
auto const res = wsClient.syncPost(request);
wsClient.disconnect();
EXPECT_EQ(res, fmt::format("{} {}", request, GetParam().expectedResponse));
}
TEST_P(WebServerAdminTest, HttpAdminCheck)
{
auto cache = MockLedgerCache();
auto const e = std::make_shared<AdminCheckExecutor>();
ClioConfigDefinition const serverConfig{
getParseAdminServerConfig(boost::json::parse(GetParam().config))
};
auto server = makeServerSync(serverConfig, ctx, dosGuardOverload, e, cache);
std::string const request = "Why hello";
uint32_t const webServerPort = serverConfig.get<uint32_t>("server.port");
auto const [status, res] = HttpSyncClient::post(
"localhost", std::to_string(webServerPort), request, GetParam().headers
);
EXPECT_EQ(res, fmt::format("{} {}", request, GetParam().expectedResponse));
EXPECT_EQ(status, boost::beast::http::status::ok);
}
INSTANTIATE_TEST_CASE_P(
WebServerAdminTestsSuit,
WebServerAdminTest,
::testing::Values(
WebServerAdminTestParams{
.config = jsonServerConfigWithAdminPassword(tests::util::generateFreePort()),
.headers = {},
.expectedResponse = "user"
},
WebServerAdminTestParams{
.config = jsonServerConfigWithAdminPassword(tests::util::generateFreePort()),
.headers = {WebHeader(http::field::authorization, "")},
.expectedResponse = "user"
},
WebServerAdminTestParams{
.config = jsonServerConfigWithAdminPassword(tests::util::generateFreePort()),
.headers = {WebHeader(http::field::authorization, "s")},
.expectedResponse = "user"
},
WebServerAdminTestParams{
.config = jsonServerConfigWithAdminPassword(tests::util::generateFreePort()),
.headers = {WebHeader(http::field::authorization, kSECRET_SHA256)},
.expectedResponse = "user"
},
WebServerAdminTestParams{
.config = jsonServerConfigWithAdminPassword(tests::util::generateFreePort()),
.headers = {WebHeader(
http::field::authorization,
fmt::format(
"{}{}",
PasswordAdminVerificationStrategy::kPASSWORD_PREFIX,
kSECRET_SHA256
)
)},
.expectedResponse = "admin"
},
WebServerAdminTestParams{
.config = jsonServerConfigWithBothAdminPasswordAndLocalAdminFalse(
tests::util::generateFreePort()
),
.headers = {WebHeader(http::field::authorization, kSECRET_SHA256)},
.expectedResponse = "user"
},
WebServerAdminTestParams{
.config = jsonServerConfigWithBothAdminPasswordAndLocalAdminFalse(
tests::util::generateFreePort()
),
.headers = {WebHeader(
http::field::authorization,
fmt::format(
"{}{}",
PasswordAdminVerificationStrategy::kPASSWORD_PREFIX,
kSECRET_SHA256
)
)},
.expectedResponse = "admin"
},
WebServerAdminTestParams{
.config = jsonServerConfigWithAdminPassword(tests::util::generateFreePort()),
.headers = {WebHeader(
http::field::authentication_info,
fmt::format(
"{}{}",
PasswordAdminVerificationStrategy::kPASSWORD_PREFIX,
kSECRET_SHA256
)
)},
.expectedResponse = "user"
},
WebServerAdminTestParams{
.config = jsonServerConfigWithLocalAdmin(tests::util::generateFreePort()),
.headers = {},
.expectedResponse = "admin"
},
WebServerAdminTestParams{
.config = jsonServerConfigWithNoSpecifiedAdmin(tests::util::generateFreePort()),
.headers = {},
.expectedResponse = "admin"
}
)
);
TEST_F(WebServerTest, AdminErrorCfgTestBothAdminPasswordAndLocalAdminSet)
{
uint32_t webServerPort = tests::util::generateFreePort();
std::string const jsonServerConfigWithBothAdminPasswordAndLocalAdmin = fmt::format(
R"JSON({{
"server":{{
"ip": "0.0.0.0",
"port": {},
"admin_password": "secret",
"local_admin": true
}}
}})JSON",
webServerPort
);
auto const e = std::make_shared<AdminCheckExecutor>();
ClioConfigDefinition const serverConfig{getParseAdminServerConfig(
boost::json::parse(jsonServerConfigWithBothAdminPasswordAndLocalAdmin)
)};
MockLedgerCache cache;
auto const result = web::makeHttpServer(serverConfig, ctx, dosGuardOverload, e, cache);
EXPECT_FALSE(result.has_value());
}
TEST_F(WebServerTest, AdminErrorCfgTestBothAdminPasswordAndLocalAdminFalse)
{
uint32_t webServerPort = tests::util::generateFreePort();
std::string const jsonServerConfigWithNoAdminPasswordAndLocalAdminFalse = fmt::format(
R"JSON({{
"server": {{
"ip": "0.0.0.0",
"port": {},
"local_admin": false
}}
}})JSON",
webServerPort
);
auto const e = std::make_shared<AdminCheckExecutor>();
ClioConfigDefinition const serverConfig{getParseAdminServerConfig(
boost::json::parse(jsonServerConfigWithNoAdminPasswordAndLocalAdminFalse)
)};
MockLedgerCache cache;
auto const result = web::makeHttpServer(serverConfig, ctx, dosGuardOverload, e, cache);
EXPECT_FALSE(result.has_value());
}
struct WebServerPrometheusTest : util::prometheus::WithPrometheus, WebServerTest {};
TEST_F(WebServerPrometheusTest, rejectedWithoutAdminPassword)
{
auto cache = MockLedgerCache();
auto const e = std::make_shared<EchoExecutor>();
uint32_t const webServerPort = tests::util::generateFreePort();
ClioConfigDefinition const serverConfig{getParseAdminServerConfig(
boost::json::parse(jsonServerConfigWithAdminPassword(webServerPort))
)};
auto server = makeServerSync(serverConfig, ctx, dosGuard, e, cache);
auto const [status, res] =
HttpSyncClient::get("localhost", std::to_string(webServerPort), "", "/metrics");
EXPECT_EQ(res, "Only admin is allowed to collect metrics");
EXPECT_EQ(status, boost::beast::http::status::unauthorized);
}
struct WebServerPrometheusDisabledTest : util::prometheus::WithPrometheusDisabled, WebServerTest {};
TEST_F(WebServerPrometheusDisabledTest, rejectedIfPrometheusIsDisabled)
{
uint32_t webServerPort = tests::util::generateFreePort();
std::string const jsonServerConfigWithDisabledPrometheus = fmt::format(
R"JSON({{
"server":{{
"ip": "0.0.0.0",
"port": {},
"admin_password": "secret",
"ws_max_sending_queue_size": 1500
}}
}})JSON",
webServerPort
);
auto cache = MockLedgerCache();
auto const e = std::make_shared<EchoExecutor>();
ClioConfigDefinition const serverConfig{
getParseAdminServerConfig(boost::json::parse(jsonServerConfigWithDisabledPrometheus))
};
auto server = makeServerSync(serverConfig, ctx, dosGuard, e, cache);
auto const [status, res] = HttpSyncClient::get(
"localhost",
std::to_string(webServerPort),
"",
"/metrics",
{WebHeader(
http::field::authorization,
fmt::format("{}{}", PasswordAdminVerificationStrategy::kPASSWORD_PREFIX, kSECRET_SHA256)
)}
);
EXPECT_EQ(res, "Prometheus is disabled in clio config");
EXPECT_EQ(status, boost::beast::http::status::forbidden);
}
TEST_F(WebServerPrometheusTest, validResponse)
{
auto cache = MockLedgerCache();
uint32_t const webServerPort = tests::util::generateFreePort();
auto& testCounter = PrometheusService::counterInt("test_counter", util::prometheus::Labels());
++testCounter;
auto const e = std::make_shared<EchoExecutor>();
ClioConfigDefinition const serverConfig{getParseAdminServerConfig(
boost::json::parse(jsonServerConfigWithAdminPassword(webServerPort))
)};
auto server = makeServerSync(serverConfig, ctx, dosGuard, e, cache);
auto const [status, res] = HttpSyncClient::get(
"localhost",
std::to_string(webServerPort),
"",
"/metrics",
{WebHeader(
http::field::authorization,
fmt::format("{}{}", PasswordAdminVerificationStrategy::kPASSWORD_PREFIX, kSECRET_SHA256)
)}
);
EXPECT_EQ(res, "# TYPE test_counter counter\ntest_counter 1\n\n");
EXPECT_EQ(status, boost::beast::http::status::ok);
}