Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/ros2_medkit_gateway/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@ if(BUILD_TESTING)
ament_add_gtest(test_update_manager test/test_update_manager.cpp)
target_link_libraries(test_update_manager gateway_lib)

# Add health handler tests
ament_add_gtest(test_health_handlers test/test_health_handlers.cpp)
target_link_libraries(test_health_handlers gateway_lib)

# Demo update backend plugin (.so for integration tests)
add_library(test_update_backend MODULE
test/demo_nodes/test_update_backend.cpp
Expand Down Expand Up @@ -408,6 +412,7 @@ if(BUILD_TESTING)
test_subscription_manager
test_cyclic_subscription_handlers
test_update_manager
test_health_handlers
)
foreach(_target ${_test_targets})
target_compile_options(${_target} PRIVATE --coverage -O0 -g)
Expand Down
224 changes: 224 additions & 0 deletions src/ros2_medkit_gateway/test/test_health_handlers.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// Copyright 2026 bburda
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <gtest/gtest.h>

#include <httplib.h>
#include <nlohmann/json.hpp>
#include <string>

#include "ros2_medkit_gateway/http/handlers/health_handlers.hpp"

using json = nlohmann::json;
using ros2_medkit_gateway::AuthConfig;
using ros2_medkit_gateway::CorsConfig;
using ros2_medkit_gateway::TlsConfig;
using ros2_medkit_gateway::handlers::HandlerContext;
using ros2_medkit_gateway::handlers::HealthHandlers;

// HealthHandlers has no dependency on GatewayNode or AuthManager:
// - handle_health only calls HandlerContext::send_json() (static)
// - handle_version_info only calls HandlerContext::send_json() (static)
// - handle_root reads ctx_.auth_config() and ctx_.tls_config() (both disabled by default)
// All tests use a null GatewayNode and null AuthManager which is safe for these handlers.

class HealthHandlersTest : public ::testing::Test {
protected:
CorsConfig cors_config_{};
AuthConfig auth_config_{}; // enabled = false by default
TlsConfig tls_config_{}; // enabled = false by default
HandlerContext ctx_{nullptr, cors_config_, auth_config_, tls_config_, nullptr};
HealthHandlers handlers_{ctx_};

httplib::Request req_;
httplib::Response res_;

HandlerContext make_context(const AuthConfig & auth, const TlsConfig & tls) {
return HandlerContext(nullptr, cors_config_, auth, tls, nullptr);
}
};

// --- handle_health ---

TEST_F(HealthHandlersTest, HandleHealthResponseContainsStatusHealthy) {
handlers_.handle_health(req_, res_);
auto body = json::parse(res_.body);
EXPECT_EQ(body["status"], "healthy");
}

TEST_F(HealthHandlersTest, HandleHealthResponseContainsTimestamp) {
handlers_.handle_health(req_, res_);
auto body = json::parse(res_.body);
EXPECT_TRUE(body.contains("timestamp"));
EXPECT_TRUE(body["timestamp"].is_number());
}

TEST_F(HealthHandlersTest, HandleHealthResponseIsValidJson) {
handlers_.handle_health(req_, res_);
EXPECT_NO_THROW(json::parse(res_.body));
}

// --- handle_version_info ---

// @verifies REQ_INTEROP_001
TEST_F(HealthHandlersTest, HandleVersionInfoContainsSovdInfoArray) {
handlers_.handle_version_info(req_, res_);
auto body = json::parse(res_.body);
ASSERT_TRUE(body.contains("sovd_info"));
ASSERT_TRUE(body["sovd_info"].is_array());
EXPECT_FALSE(body["sovd_info"].empty());
}

// @verifies REQ_INTEROP_001
TEST_F(HealthHandlersTest, HandleVersionInfoSovdEntryHasVersionField) {
handlers_.handle_version_info(req_, res_);
auto body = json::parse(res_.body);
auto & entry = body["sovd_info"][0];
EXPECT_TRUE(entry.contains("version"));
EXPECT_TRUE(entry["version"].is_string());
EXPECT_FALSE(entry["version"].get<std::string>().empty());
}

// @verifies REQ_INTEROP_001
TEST_F(HealthHandlersTest, HandleVersionInfoSovdEntryHasBaseUri) {
handlers_.handle_version_info(req_, res_);
auto body = json::parse(res_.body);
auto & entry = body["sovd_info"][0];
EXPECT_TRUE(entry.contains("base_uri"));
}

// @verifies REQ_INTEROP_001
TEST_F(HealthHandlersTest, HandleVersionInfoSovdEntryHasVendorInfo) {
handlers_.handle_version_info(req_, res_);
auto body = json::parse(res_.body);
auto & entry = body["sovd_info"][0];
EXPECT_TRUE(entry.contains("vendor_info"));
EXPECT_TRUE(entry["vendor_info"].contains("name"));
EXPECT_EQ(entry["vendor_info"]["name"], "ros2_medkit");
}

// --- handle_root ---

// @verifies REQ_INTEROP_010
TEST_F(HealthHandlersTest, HandleRootResponseContainsRequiredTopLevelFields) {
handlers_.handle_root(req_, res_);
auto body = json::parse(res_.body);
EXPECT_TRUE(body.contains("name"));
EXPECT_FALSE(body["name"].get<std::string>().empty());
EXPECT_TRUE(body.contains("version"));
EXPECT_FALSE(body["version"].get<std::string>().empty());
EXPECT_TRUE(body.contains("api_base"));
EXPECT_FALSE(body["api_base"].get<std::string>().empty());
EXPECT_TRUE(body.contains("endpoints"));
EXPECT_TRUE(body.contains("capabilities"));
}

// @verifies REQ_INTEROP_010
TEST_F(HealthHandlersTest, HandleRootEndpointsIsNonEmptyArray) {
handlers_.handle_root(req_, res_);
auto body = json::parse(res_.body);
ASSERT_TRUE(body["endpoints"].is_array());
EXPECT_FALSE(body["endpoints"].empty());
}

// @verifies REQ_INTEROP_010
TEST_F(HealthHandlersTest, HandleRootCapabilitiesContainsDiscovery) {
handlers_.handle_root(req_, res_);
auto body = json::parse(res_.body);
auto & caps = body["capabilities"];
EXPECT_TRUE(caps.contains("discovery"));
EXPECT_TRUE(caps["discovery"].get<bool>());
}

// @verifies REQ_INTEROP_010
TEST_F(HealthHandlersTest, HandleRootAuthDisabledNoAuthEndpoints) {
// With auth disabled (default), auth endpoints must not appear in the list
handlers_.handle_root(req_, res_);
auto body = json::parse(res_.body);
for (const auto & ep : body["endpoints"]) {
EXPECT_EQ(ep.get<std::string>().find("/auth/"), std::string::npos)
<< "Unexpected auth endpoint when auth is disabled: " << ep;
}
}

// @verifies REQ_INTEROP_010
TEST_F(HealthHandlersTest, HandleRootCapabilitiesAuthDisabled) {
handlers_.handle_root(req_, res_);
auto body = json::parse(res_.body);
EXPECT_FALSE(body["capabilities"]["authentication"].get<bool>());
}

// @verifies REQ_INTEROP_010
TEST_F(HealthHandlersTest, HandleRootCapabilitiesTlsDisabled) {
handlers_.handle_root(req_, res_);
auto body = json::parse(res_.body);
EXPECT_FALSE(body["capabilities"]["tls"].get<bool>());
EXPECT_FALSE(body.contains("tls"));
}

// @verifies REQ_INTEROP_010
TEST_F(HealthHandlersTest, HandleRootAuthEnabledAddsAuthEndpoints) {
AuthConfig auth_enabled{};
auth_enabled.enabled = true;
auto ctx_auth = make_context(auth_enabled, tls_config_);
HealthHandlers handlers_auth(ctx_auth);

handlers_auth.handle_root(req_, res_);
auto body = json::parse(res_.body);

bool has_auth_endpoint = false;
for (const auto & ep : body["endpoints"]) {
if (ep.get<std::string>().find("/auth/") != std::string::npos) {
has_auth_endpoint = true;
break;
}
}
EXPECT_TRUE(has_auth_endpoint);
EXPECT_TRUE(body["capabilities"]["authentication"].get<bool>());
}

// @verifies REQ_INTEROP_010
TEST_F(HealthHandlersTest, HandleRootAuthEnabledIncludesAuthMetadataBlock) {
AuthConfig auth_enabled{};
auth_enabled.enabled = true;
auth_enabled.require_auth_for = ros2_medkit_gateway::AuthRequirement::ALL;
auth_enabled.jwt_algorithm = ros2_medkit_gateway::JwtAlgorithm::HS256;
auto ctx_auth = make_context(auth_enabled, tls_config_);
HealthHandlers handlers_auth(ctx_auth);

handlers_auth.handle_root(req_, res_);
auto body = json::parse(res_.body);

ASSERT_TRUE(body.contains("auth"));
EXPECT_TRUE(body["auth"]["enabled"].get<bool>());
EXPECT_EQ(body["auth"]["algorithm"], "HS256");
EXPECT_EQ(body["auth"]["require_auth_for"], "all");
}

// @verifies REQ_INTEROP_010
TEST_F(HealthHandlersTest, HandleRootTlsEnabledIncludesTlsMetadataBlock) {
TlsConfig tls_enabled{};
tls_enabled.enabled = true;
tls_enabled.min_version = "1.3";
auto ctx_tls = make_context(auth_config_, tls_enabled);
HealthHandlers handlers_tls(ctx_tls);

handlers_tls.handle_root(req_, res_);
auto body = json::parse(res_.body);

ASSERT_TRUE(body.contains("tls"));
EXPECT_TRUE(body["tls"]["enabled"].get<bool>());
EXPECT_EQ(body["tls"]["min_version"], "1.3");
EXPECT_TRUE(body["capabilities"]["tls"].get<bool>());
}
Loading