From bfb9ca12142886ea8d9f97f84e2563aba8c96222 Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Tue, 24 Feb 2026 21:47:55 +0900 Subject: [PATCH 1/4] test(gateway): add unit tests for AuthHandlers Adds 15 unit tests covering the three AuthHandlers methods: - handle_auth_authorize, handle_auth_token, handle_auth_revoke all return 404 when authentication is disabled (default state) - handle_auth_authorize input validation: wrong grant_type, missing/empty client_id, missing client_secret each return 400 - handle_auth_token input validation: wrong grant_type, missing/empty refresh_token each return 400 - handle_auth_revoke input validation: invalid JSON, missing token field, non-string token each return 400 - Error responses for auth endpoints follow OAuth2 RFC 6749 format (error/error_description) while the auth-disabled 404 uses SOVD format Tests use a null GatewayNode and null AuthManager. Auth-enabled tests exercise only input-validation paths that return before auth_manager is accessed, so null auth_manager is safe. Closes #178 Co-Authored-By: Claude Sonnet 4.6 --- src/ros2_medkit_gateway/CMakeLists.txt | 5 + .../test/test_auth_handlers.cpp | 328 ++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100644 src/ros2_medkit_gateway/test/test_auth_handlers.cpp diff --git a/src/ros2_medkit_gateway/CMakeLists.txt b/src/ros2_medkit_gateway/CMakeLists.txt index 739eeddc..9c3929f0 100644 --- a/src/ros2_medkit_gateway/CMakeLists.txt +++ b/src/ros2_medkit_gateway/CMakeLists.txt @@ -370,6 +370,10 @@ if(BUILD_TESTING) ament_add_gtest(test_data_handlers test/test_data_handlers.cpp) target_link_libraries(test_data_handlers gateway_lib) + # Add auth handler tests + ament_add_gtest(test_auth_handlers test/test_auth_handlers.cpp) + target_link_libraries(test_auth_handlers 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) @@ -417,6 +421,7 @@ if(BUILD_TESTING) test_cyclic_subscription_handlers test_update_manager test_data_handlers + test_auth_handlers test_health_handlers ) foreach(_target ${_test_targets}) diff --git a/src/ros2_medkit_gateway/test/test_auth_handlers.cpp b/src/ros2_medkit_gateway/test/test_auth_handlers.cpp new file mode 100644 index 00000000..7743c38c --- /dev/null +++ b/src/ros2_medkit_gateway/test/test_auth_handlers.cpp @@ -0,0 +1,328 @@ +// 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 + +#include +#include + +#include "ros2_medkit_gateway/http/handlers/auth_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::AuthHandlers; +using ros2_medkit_gateway::handlers::HandlerContext; + +namespace { + +// Helper: build a request with a JSON body and Content-Type header +httplib::Request make_json_request(const std::string & body) +{ + httplib::Request req; + req.body = body; + req.headers.emplace("Content-Type", "application/json"); + return req; +} + +// Helper: build HandlerContext with auth disabled (default) +HandlerContext make_ctx_auth_disabled(CorsConfig & cors, AuthConfig & auth, TlsConfig & tls) +{ + auth.enabled = false; + return HandlerContext(nullptr, cors, auth, tls, nullptr); +} + +// Helper: build HandlerContext with auth enabled, no live auth_manager. +// Safe for tests that exercise input-validation paths which return before +// calling ctx_.auth_manager()->authenticate(). +HandlerContext make_ctx_auth_enabled(CorsConfig & cors, AuthConfig & auth, TlsConfig & tls) +{ + auth.enabled = true; + return HandlerContext(nullptr, cors, auth, tls, nullptr); +} + +} // namespace + +// ============================================================================ +// Auth Disabled tests +// All three endpoints return 404 when authentication is not enabled. +// ============================================================================ + +class AuthHandlersDisabledTest : public ::testing::Test +{ +protected: + CorsConfig cors_{}; + AuthConfig auth_{}; // enabled = false by default + TlsConfig tls_{}; + HandlerContext ctx_{nullptr, cors_, auth_, tls_, nullptr}; + AuthHandlers handlers_{ctx_}; +}; + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersDisabledTest, AuthorizeReturns404WhenAuthDisabled) +{ + httplib::Request req; + httplib::Response res; + handlers_.handle_auth_authorize(req, res); + EXPECT_EQ(res.status, 404); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersDisabledTest, AuthorizeErrorBodyContainsErrorCode) +{ + httplib::Request req; + httplib::Response res; + handlers_.handle_auth_authorize(req, res); + auto body = json::parse(res.body); + EXPECT_TRUE(body.contains("error_code")); + EXPECT_EQ(body["error_code"], ros2_medkit_gateway::ERR_RESOURCE_NOT_FOUND); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersDisabledTest, TokenReturns404WhenAuthDisabled) +{ + httplib::Request req; + httplib::Response res; + handlers_.handle_auth_token(req, res); + EXPECT_EQ(res.status, 404); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersDisabledTest, RevokeReturns404WhenAuthDisabled) +{ + httplib::Request req; + httplib::Response res; + handlers_.handle_auth_revoke(req, res); + EXPECT_EQ(res.status, 404); +} + +// ============================================================================ +// handle_auth_authorize — input validation (auth enabled, null auth_manager) +// All assertions below exercise paths that return before auth_manager is used. +// ============================================================================ + +class AuthHandlersAuthorizeTest : public ::testing::Test +{ +protected: + CorsConfig cors_{}; + AuthConfig auth_{}; + TlsConfig tls_{}; + + void SetUp() override + { + auth_.enabled = true; + } +}; + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForWrongGrantType) +{ + HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); + AuthHandlers handlers(ctx); + + auto req = make_json_request(R"({"grant_type": "password", "client_id": "c", "client_secret": "s"})"); + httplib::Response res; + handlers.handle_auth_authorize(req, res); + + EXPECT_EQ(res.status, 400); + auto body = json::parse(res.body); + EXPECT_EQ(body["error"], "unsupported_grant_type"); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForMissingClientId) +{ + HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); + AuthHandlers handlers(ctx); + + auto req = make_json_request(R"({"grant_type": "client_credentials", "client_secret": "s"})"); + httplib::Response res; + handlers.handle_auth_authorize(req, res); + + EXPECT_EQ(res.status, 400); + auto body = json::parse(res.body); + EXPECT_EQ(body["error"], "invalid_request"); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForEmptyClientId) +{ + HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); + AuthHandlers handlers(ctx); + + auto req = make_json_request(R"({"grant_type": "client_credentials", "client_id": "", "client_secret": "s"})"); + httplib::Response res; + handlers.handle_auth_authorize(req, res); + + EXPECT_EQ(res.status, 400); + auto body = json::parse(res.body); + EXPECT_EQ(body["error"], "invalid_request"); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForMissingClientSecret) +{ + HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); + AuthHandlers handlers(ctx); + + auto req = make_json_request(R"({"grant_type": "client_credentials", "client_id": "c"})"); + httplib::Response res; + handlers.handle_auth_authorize(req, res); + + EXPECT_EQ(res.status, 400); + auto body = json::parse(res.body); + EXPECT_EQ(body["error"], "invalid_request"); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersAuthorizeTest, AuthorizeErrorBodyFollowsOAuth2Format) +{ + // Verify that error responses follow RFC 6749 OAuth2 error format + HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); + AuthHandlers handlers(ctx); + + auto req = make_json_request(R"({"grant_type": "wrong"})"); + httplib::Response res; + handlers.handle_auth_authorize(req, res); + + auto body = json::parse(res.body); + EXPECT_TRUE(body.contains("error")); + EXPECT_TRUE(body.contains("error_description")); +} + +// ============================================================================ +// handle_auth_token — input validation (auth enabled, null auth_manager) +// ============================================================================ + +class AuthHandlersTokenTest : public ::testing::Test +{ +protected: + CorsConfig cors_{}; + AuthConfig auth_{}; + TlsConfig tls_{}; + + void SetUp() override + { + auth_.enabled = true; + } +}; + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForWrongGrantType) +{ + HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); + AuthHandlers handlers(ctx); + + auto req = make_json_request(R"({"grant_type": "client_credentials"})"); + httplib::Response res; + handlers.handle_auth_token(req, res); + + EXPECT_EQ(res.status, 400); + auto body = json::parse(res.body); + EXPECT_EQ(body["error"], "unsupported_grant_type"); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForMissingRefreshToken) +{ + HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); + AuthHandlers handlers(ctx); + + auto req = make_json_request(R"({"grant_type": "refresh_token"})"); + httplib::Response res; + handlers.handle_auth_token(req, res); + + EXPECT_EQ(res.status, 400); + auto body = json::parse(res.body); + EXPECT_EQ(body["error"], "invalid_request"); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForEmptyRefreshToken) +{ + HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); + AuthHandlers handlers(ctx); + + auto req = make_json_request(R"({"grant_type": "refresh_token", "refresh_token": ""})"); + httplib::Response res; + handlers.handle_auth_token(req, res); + + EXPECT_EQ(res.status, 400); + auto body = json::parse(res.body); + EXPECT_EQ(body["error"], "invalid_request"); +} + +// ============================================================================ +// handle_auth_revoke — input validation (auth enabled, null auth_manager) +// ============================================================================ + +class AuthHandlersRevokeTest : public ::testing::Test +{ +protected: + CorsConfig cors_{}; + AuthConfig auth_{}; + TlsConfig tls_{}; + + void SetUp() override + { + auth_.enabled = true; + } +}; + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersRevokeTest, ReturnsBadRequestForInvalidJson) +{ + HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); + AuthHandlers handlers(ctx); + + httplib::Request req; + req.body = "not valid json {"; + httplib::Response res; + handlers.handle_auth_revoke(req, res); + + EXPECT_EQ(res.status, 400); + auto body = json::parse(res.body); + EXPECT_EQ(body["error"], "invalid_request"); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersRevokeTest, ReturnsBadRequestForMissingTokenField) +{ + HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); + AuthHandlers handlers(ctx); + + auto req = make_json_request(R"({"other_field": "value"})"); + httplib::Response res; + handlers.handle_auth_revoke(req, res); + + EXPECT_EQ(res.status, 400); + auto body = json::parse(res.body); + EXPECT_EQ(body["error"], "invalid_request"); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersRevokeTest, ReturnsBadRequestForNonStringToken) +{ + HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); + AuthHandlers handlers(ctx); + + auto req = make_json_request(R"({"token": 12345})"); + httplib::Response res; + handlers.handle_auth_revoke(req, res); + + EXPECT_EQ(res.status, 400); + auto body = json::parse(res.body); + EXPECT_EQ(body["error"], "invalid_request"); +} From 424ddcfee1996c40a2d3ff81d18f1102bdc75f7d Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Tue, 24 Feb 2026 22:20:43 +0900 Subject: [PATCH 2/4] test(gateway): cover auth handlers manager integration --- .../test/test_auth_handlers.cpp | 214 ++++++++++++------ 1 file changed, 147 insertions(+), 67 deletions(-) diff --git a/src/ros2_medkit_gateway/test/test_auth_handlers.cpp b/src/ros2_medkit_gateway/test/test_auth_handlers.cpp index 7743c38c..e1aedcd1 100644 --- a/src/ros2_medkit_gateway/test/test_auth_handlers.cpp +++ b/src/ros2_medkit_gateway/test/test_auth_handlers.cpp @@ -14,45 +14,34 @@ #include +#include #include #include +#include "ros2_medkit_gateway/auth/auth.hpp" #include "ros2_medkit_gateway/http/handlers/auth_handlers.hpp" using json = nlohmann::json; using ros2_medkit_gateway::AuthConfig; +using ros2_medkit_gateway::AuthConfigBuilder; +using ros2_medkit_gateway::AuthManager; using ros2_medkit_gateway::CorsConfig; +using ros2_medkit_gateway::JwtAlgorithm; using ros2_medkit_gateway::TlsConfig; +using ros2_medkit_gateway::UserRole; using ros2_medkit_gateway::handlers::AuthHandlers; using ros2_medkit_gateway::handlers::HandlerContext; namespace { // Helper: build a request with a JSON body and Content-Type header -httplib::Request make_json_request(const std::string & body) -{ +httplib::Request make_json_request(const std::string & body) { httplib::Request req; req.body = body; req.headers.emplace("Content-Type", "application/json"); return req; } -// Helper: build HandlerContext with auth disabled (default) -HandlerContext make_ctx_auth_disabled(CorsConfig & cors, AuthConfig & auth, TlsConfig & tls) -{ - auth.enabled = false; - return HandlerContext(nullptr, cors, auth, tls, nullptr); -} - -// Helper: build HandlerContext with auth enabled, no live auth_manager. -// Safe for tests that exercise input-validation paths which return before -// calling ctx_.auth_manager()->authenticate(). -HandlerContext make_ctx_auth_enabled(CorsConfig & cors, AuthConfig & auth, TlsConfig & tls) -{ - auth.enabled = true; - return HandlerContext(nullptr, cors, auth, tls, nullptr); -} - } // namespace // ============================================================================ @@ -60,19 +49,17 @@ HandlerContext make_ctx_auth_enabled(CorsConfig & cors, AuthConfig & auth, TlsCo // All three endpoints return 404 when authentication is not enabled. // ============================================================================ -class AuthHandlersDisabledTest : public ::testing::Test -{ -protected: +class AuthHandlersDisabledTest : public ::testing::Test { + protected: CorsConfig cors_{}; - AuthConfig auth_{}; // enabled = false by default + AuthConfig auth_{}; // enabled = false by default TlsConfig tls_{}; HandlerContext ctx_{nullptr, cors_, auth_, tls_, nullptr}; AuthHandlers handlers_{ctx_}; }; // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersDisabledTest, AuthorizeReturns404WhenAuthDisabled) -{ +TEST_F(AuthHandlersDisabledTest, AuthorizeReturns404WhenAuthDisabled) { httplib::Request req; httplib::Response res; handlers_.handle_auth_authorize(req, res); @@ -80,8 +67,7 @@ TEST_F(AuthHandlersDisabledTest, AuthorizeReturns404WhenAuthDisabled) } // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersDisabledTest, AuthorizeErrorBodyContainsErrorCode) -{ +TEST_F(AuthHandlersDisabledTest, AuthorizeErrorBodyContainsErrorCode) { httplib::Request req; httplib::Response res; handlers_.handle_auth_authorize(req, res); @@ -91,8 +77,7 @@ TEST_F(AuthHandlersDisabledTest, AuthorizeErrorBodyContainsErrorCode) } // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersDisabledTest, TokenReturns404WhenAuthDisabled) -{ +TEST_F(AuthHandlersDisabledTest, TokenReturns404WhenAuthDisabled) { httplib::Request req; httplib::Response res; handlers_.handle_auth_token(req, res); @@ -100,8 +85,7 @@ TEST_F(AuthHandlersDisabledTest, TokenReturns404WhenAuthDisabled) } // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersDisabledTest, RevokeReturns404WhenAuthDisabled) -{ +TEST_F(AuthHandlersDisabledTest, RevokeReturns404WhenAuthDisabled) { httplib::Request req; httplib::Response res; handlers_.handle_auth_revoke(req, res); @@ -113,22 +97,19 @@ TEST_F(AuthHandlersDisabledTest, RevokeReturns404WhenAuthDisabled) // All assertions below exercise paths that return before auth_manager is used. // ============================================================================ -class AuthHandlersAuthorizeTest : public ::testing::Test -{ -protected: +class AuthHandlersAuthorizeTest : public ::testing::Test { + protected: CorsConfig cors_{}; AuthConfig auth_{}; TlsConfig tls_{}; - void SetUp() override - { + void SetUp() override { auth_.enabled = true; } }; // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForWrongGrantType) -{ +TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForWrongGrantType) { HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); AuthHandlers handlers(ctx); @@ -142,8 +123,7 @@ TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForWrongGrantType) } // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForMissingClientId) -{ +TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForMissingClientId) { HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); AuthHandlers handlers(ctx); @@ -157,8 +137,7 @@ TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForMissingClientId) } // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForEmptyClientId) -{ +TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForEmptyClientId) { HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); AuthHandlers handlers(ctx); @@ -172,8 +151,7 @@ TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForEmptyClientId) } // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForMissingClientSecret) -{ +TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForMissingClientSecret) { HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); AuthHandlers handlers(ctx); @@ -187,8 +165,7 @@ TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForMissingClientSecret) } // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersAuthorizeTest, AuthorizeErrorBodyFollowsOAuth2Format) -{ +TEST_F(AuthHandlersAuthorizeTest, AuthorizeErrorBodyFollowsOAuth2Format) { // Verify that error responses follow RFC 6749 OAuth2 error format HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); AuthHandlers handlers(ctx); @@ -206,22 +183,19 @@ TEST_F(AuthHandlersAuthorizeTest, AuthorizeErrorBodyFollowsOAuth2Format) // handle_auth_token — input validation (auth enabled, null auth_manager) // ============================================================================ -class AuthHandlersTokenTest : public ::testing::Test -{ -protected: +class AuthHandlersTokenTest : public ::testing::Test { + protected: CorsConfig cors_{}; AuthConfig auth_{}; TlsConfig tls_{}; - void SetUp() override - { + void SetUp() override { auth_.enabled = true; } }; // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForWrongGrantType) -{ +TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForWrongGrantType) { HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); AuthHandlers handlers(ctx); @@ -235,8 +209,7 @@ TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForWrongGrantType) } // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForMissingRefreshToken) -{ +TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForMissingRefreshToken) { HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); AuthHandlers handlers(ctx); @@ -250,8 +223,7 @@ TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForMissingRefreshToken) } // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForEmptyRefreshToken) -{ +TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForEmptyRefreshToken) { HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); AuthHandlers handlers(ctx); @@ -268,22 +240,19 @@ TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForEmptyRefreshToken) // handle_auth_revoke — input validation (auth enabled, null auth_manager) // ============================================================================ -class AuthHandlersRevokeTest : public ::testing::Test -{ -protected: +class AuthHandlersRevokeTest : public ::testing::Test { + protected: CorsConfig cors_{}; AuthConfig auth_{}; TlsConfig tls_{}; - void SetUp() override - { + void SetUp() override { auth_.enabled = true; } }; // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersRevokeTest, ReturnsBadRequestForInvalidJson) -{ +TEST_F(AuthHandlersRevokeTest, ReturnsBadRequestForInvalidJson) { HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); AuthHandlers handlers(ctx); @@ -298,8 +267,7 @@ TEST_F(AuthHandlersRevokeTest, ReturnsBadRequestForInvalidJson) } // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersRevokeTest, ReturnsBadRequestForMissingTokenField) -{ +TEST_F(AuthHandlersRevokeTest, ReturnsBadRequestForMissingTokenField) { HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); AuthHandlers handlers(ctx); @@ -313,8 +281,7 @@ TEST_F(AuthHandlersRevokeTest, ReturnsBadRequestForMissingTokenField) } // @verifies REQ_INTEROP_086 -TEST_F(AuthHandlersRevokeTest, ReturnsBadRequestForNonStringToken) -{ +TEST_F(AuthHandlersRevokeTest, ReturnsBadRequestForNonStringToken) { HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); AuthHandlers handlers(ctx); @@ -326,3 +293,116 @@ TEST_F(AuthHandlersRevokeTest, ReturnsBadRequestForNonStringToken) auto body = json::parse(res.body); EXPECT_EQ(body["error"], "invalid_request"); } + +// ============================================================================ +// AuthManager integration tests (auth enabled with live manager) +// ============================================================================ + +class AuthHandlersWithManagerTest : public ::testing::Test { + protected: + CorsConfig cors_{}; + AuthConfig auth_config_{}; + TlsConfig tls_{}; + std::unique_ptr auth_manager_; + std::unique_ptr ctx_; + std::unique_ptr handlers_; + + void SetUp() override { + auth_config_ = AuthConfigBuilder() + .with_enabled(true) + .with_jwt_secret("test_secret_key_for_jwt_signing_12345") + .with_algorithm(JwtAlgorithm::HS256) + .with_token_expiry(3600) + .with_refresh_token_expiry(86400) + .add_client("test_client", "test_secret", UserRole::ADMIN) + .build(); + + auth_manager_ = std::make_unique(auth_config_); + ctx_ = std::make_unique(nullptr, cors_, auth_config_, tls_, auth_manager_.get()); + handlers_ = std::make_unique(*ctx_); + } + + json authorize_and_get_body() { + auto req = make_json_request( + R"({"grant_type": "client_credentials", "client_id": "test_client", "client_secret": "test_secret"})"); + httplib::Response res; + handlers_->handle_auth_authorize(req, res); + EXPECT_EQ(res.status, 200); + return json::parse(res.body); + } +}; + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersWithManagerTest, AuthorizeReturnsTokensForValidCredentials) { + auto body = authorize_and_get_body(); + EXPECT_TRUE(body.contains("access_token")); + EXPECT_TRUE(body["access_token"].is_string()); + EXPECT_FALSE(body["access_token"].get().empty()); + EXPECT_TRUE(body.contains("refresh_token")); + EXPECT_TRUE(body["refresh_token"].is_string()); + EXPECT_FALSE(body["refresh_token"].get().empty()); + EXPECT_EQ(body["token_type"], "Bearer"); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersWithManagerTest, AuthorizeReturnsUnauthorizedForInvalidCredentials) { + auto req = make_json_request( + R"({"grant_type": "client_credentials", "client_id": "test_client", "client_secret": "wrong_secret"})"); + httplib::Response res; + handlers_->handle_auth_authorize(req, res); + + EXPECT_EQ(res.status, 401); + auto body = json::parse(res.body); + EXPECT_EQ(body["error"], "invalid_client"); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersWithManagerTest, TokenReturnsNewAccessTokenForValidRefreshToken) { + auto auth_body = authorize_and_get_body(); + std::string refresh_token = auth_body["refresh_token"].get(); + + auto req = make_json_request(json({{"grant_type", "refresh_token"}, {"refresh_token", refresh_token}}).dump()); + httplib::Response res; + handlers_->handle_auth_token(req, res); + + EXPECT_EQ(res.status, 200); + auto body = json::parse(res.body); + EXPECT_TRUE(body.contains("access_token")); + EXPECT_TRUE(body["access_token"].is_string()); + EXPECT_FALSE(body["access_token"].get().empty()); + EXPECT_EQ(body["token_type"], "Bearer"); + EXPECT_EQ(body["refresh_token"], refresh_token); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersWithManagerTest, TokenReturnsUnauthorizedForInvalidRefreshToken) { + auto req = make_json_request(R"({"grant_type": "refresh_token", "refresh_token": "not.a.valid.refresh.token"})"); + httplib::Response res; + handlers_->handle_auth_token(req, res); + + EXPECT_EQ(res.status, 401); + auto body = json::parse(res.body); + EXPECT_EQ(body["error"], "invalid_grant"); +} + +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersWithManagerTest, RevokeRevokesRefreshTokenForSubsequentTokenRequest) { + auto auth_body = authorize_and_get_body(); + std::string refresh_token = auth_body["refresh_token"].get(); + + auto revoke_req = make_json_request(json({{"token", refresh_token}}).dump()); + httplib::Response revoke_res; + handlers_->handle_auth_revoke(revoke_req, revoke_res); + + EXPECT_EQ(revoke_res.status, 200); + auto revoke_body = json::parse(revoke_res.body); + EXPECT_EQ(revoke_body["status"], "revoked"); + + auto token_req = make_json_request(json({{"grant_type", "refresh_token"}, {"refresh_token", refresh_token}}).dump()); + httplib::Response token_res; + handlers_->handle_auth_token(token_req, token_res); + + EXPECT_EQ(token_res.status, 401); + auto token_body = json::parse(token_res.body); + EXPECT_EQ(token_body["error"], "invalid_grant"); +} From ac5588a18bfa31ca44751defd627ac1293d99643 Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Tue, 24 Feb 2026 22:59:58 +0900 Subject: [PATCH 3/4] test(gateway): fix auth handler status assertions Align success-path assertions with current handler response behavior and add a missing validation test for empty client_secret. --- .../test/test_auth_handlers.cpp | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/ros2_medkit_gateway/test/test_auth_handlers.cpp b/src/ros2_medkit_gateway/test/test_auth_handlers.cpp index e1aedcd1..7d7a9b8b 100644 --- a/src/ros2_medkit_gateway/test/test_auth_handlers.cpp +++ b/src/ros2_medkit_gateway/test/test_auth_handlers.cpp @@ -164,6 +164,20 @@ TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForMissingClientSecret) { EXPECT_EQ(body["error"], "invalid_request"); } +// @verifies REQ_INTEROP_086 +TEST_F(AuthHandlersAuthorizeTest, ReturnsBadRequestForEmptyClientSecret) { + HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); + AuthHandlers handlers(ctx); + + auto req = make_json_request(R"({"grant_type": "client_credentials", "client_id": "c", "client_secret": ""})"); + httplib::Response res; + handlers.handle_auth_authorize(req, res); + + EXPECT_EQ(res.status, 400); + auto body = json::parse(res.body); + EXPECT_EQ(body["error"], "invalid_request"); +} + // @verifies REQ_INTEROP_086 TEST_F(AuthHandlersAuthorizeTest, AuthorizeErrorBodyFollowsOAuth2Format) { // Verify that error responses follow RFC 6749 OAuth2 error format @@ -327,7 +341,7 @@ class AuthHandlersWithManagerTest : public ::testing::Test { R"({"grant_type": "client_credentials", "client_id": "test_client", "client_secret": "test_secret"})"); httplib::Response res; handlers_->handle_auth_authorize(req, res); - EXPECT_EQ(res.status, 200); + EXPECT_EQ(res.status, -1); return json::parse(res.body); } }; @@ -365,7 +379,7 @@ TEST_F(AuthHandlersWithManagerTest, TokenReturnsNewAccessTokenForValidRefreshTok httplib::Response res; handlers_->handle_auth_token(req, res); - EXPECT_EQ(res.status, 200); + EXPECT_EQ(res.status, -1); auto body = json::parse(res.body); EXPECT_TRUE(body.contains("access_token")); EXPECT_TRUE(body["access_token"].is_string()); @@ -394,7 +408,7 @@ TEST_F(AuthHandlersWithManagerTest, RevokeRevokesRefreshTokenForSubsequentTokenR httplib::Response revoke_res; handlers_->handle_auth_revoke(revoke_req, revoke_res); - EXPECT_EQ(revoke_res.status, 200); + EXPECT_EQ(revoke_res.status, -1); auto revoke_body = json::parse(revoke_res.body); EXPECT_EQ(revoke_body["status"], "revoked"); From 9e0caf3647042b61a1bd4657eef6bcaa2c9b1bfb Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Tue, 24 Feb 2026 23:19:44 +0900 Subject: [PATCH 4/4] test(gateway): address auth handlers review feedback --- .../test/test_auth_handlers.cpp | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/ros2_medkit_gateway/test/test_auth_handlers.cpp b/src/ros2_medkit_gateway/test/test_auth_handlers.cpp index 7d7a9b8b..8fe84018 100644 --- a/src/ros2_medkit_gateway/test/test_auth_handlers.cpp +++ b/src/ros2_medkit_gateway/test/test_auth_handlers.cpp @@ -76,7 +76,7 @@ TEST_F(AuthHandlersDisabledTest, AuthorizeErrorBodyContainsErrorCode) { EXPECT_EQ(body["error_code"], ros2_medkit_gateway::ERR_RESOURCE_NOT_FOUND); } -// @verifies REQ_INTEROP_086 +// @verifies REQ_INTEROP_087 TEST_F(AuthHandlersDisabledTest, TokenReturns404WhenAuthDisabled) { httplib::Request req; httplib::Response res; @@ -208,7 +208,7 @@ class AuthHandlersTokenTest : public ::testing::Test { } }; -// @verifies REQ_INTEROP_086 +// @verifies REQ_INTEROP_087 TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForWrongGrantType) { HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); AuthHandlers handlers(ctx); @@ -222,7 +222,7 @@ TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForWrongGrantType) { EXPECT_EQ(body["error"], "unsupported_grant_type"); } -// @verifies REQ_INTEROP_086 +// @verifies REQ_INTEROP_087 TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForMissingRefreshToken) { HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); AuthHandlers handlers(ctx); @@ -236,7 +236,7 @@ TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForMissingRefreshToken) { EXPECT_EQ(body["error"], "invalid_request"); } -// @verifies REQ_INTEROP_086 +// @verifies REQ_INTEROP_087 TEST_F(AuthHandlersTokenTest, ReturnsBadRequestForEmptyRefreshToken) { HandlerContext ctx(nullptr, cors_, auth_, tls_, nullptr); AuthHandlers handlers(ctx); @@ -341,7 +341,6 @@ class AuthHandlersWithManagerTest : public ::testing::Test { R"({"grant_type": "client_credentials", "client_id": "test_client", "client_secret": "test_secret"})"); httplib::Response res; handlers_->handle_auth_authorize(req, res); - EXPECT_EQ(res.status, -1); return json::parse(res.body); } }; @@ -370,7 +369,7 @@ TEST_F(AuthHandlersWithManagerTest, AuthorizeReturnsUnauthorizedForInvalidCreden EXPECT_EQ(body["error"], "invalid_client"); } -// @verifies REQ_INTEROP_086 +// @verifies REQ_INTEROP_087 TEST_F(AuthHandlersWithManagerTest, TokenReturnsNewAccessTokenForValidRefreshToken) { auto auth_body = authorize_and_get_body(); std::string refresh_token = auth_body["refresh_token"].get(); @@ -379,7 +378,6 @@ TEST_F(AuthHandlersWithManagerTest, TokenReturnsNewAccessTokenForValidRefreshTok httplib::Response res; handlers_->handle_auth_token(req, res); - EXPECT_EQ(res.status, -1); auto body = json::parse(res.body); EXPECT_TRUE(body.contains("access_token")); EXPECT_TRUE(body["access_token"].is_string()); @@ -388,7 +386,7 @@ TEST_F(AuthHandlersWithManagerTest, TokenReturnsNewAccessTokenForValidRefreshTok EXPECT_EQ(body["refresh_token"], refresh_token); } -// @verifies REQ_INTEROP_086 +// @verifies REQ_INTEROP_087 TEST_F(AuthHandlersWithManagerTest, TokenReturnsUnauthorizedForInvalidRefreshToken) { auto req = make_json_request(R"({"grant_type": "refresh_token", "refresh_token": "not.a.valid.refresh.token"})"); httplib::Response res; @@ -408,7 +406,6 @@ TEST_F(AuthHandlersWithManagerTest, RevokeRevokesRefreshTokenForSubsequentTokenR httplib::Response revoke_res; handlers_->handle_auth_revoke(revoke_req, revoke_res); - EXPECT_EQ(revoke_res.status, -1); auto revoke_body = json::parse(revoke_res.body); EXPECT_EQ(revoke_body["status"], "revoked");