From a2285159b3a1ad3c413ac0399df410e9904d01d7 Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Sat, 21 Mar 2026 22:36:49 +0900 Subject: [PATCH 1/3] Add app is-located-on discovery endpoint --- .../http/handlers/discovery_handlers.hpp | 5 ++ .../src/http/handlers/discovery_handlers.cpp | 60 +++++++++++++++++++ .../src/http/rest_server.cpp | 7 +++ .../test/test_discovery_handlers.cpp | 58 +++++++++++++++++- .../test/test_gateway_node.cpp | 52 ++++++++++++++++ 5 files changed, 180 insertions(+), 2 deletions(-) diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handlers/discovery_handlers.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handlers/discovery_handlers.hpp index 20cc0c0c..98566cc7 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handlers/discovery_handlers.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handlers/discovery_handlers.hpp @@ -125,6 +125,11 @@ class DiscoveryHandlers { */ void handle_app_depends_on(const httplib::Request & req, httplib::Response & res); + /** + * @brief Handle GET /apps/{app-id}/is-located-on - get parent component. + */ + void handle_app_is_located_on(const httplib::Request & req, httplib::Response & res); + // ========================================================================= // Function endpoints // ========================================================================= diff --git a/src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp b/src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp index b9e01dc1..ecb43e88 100644 --- a/src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp +++ b/src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp @@ -951,6 +951,66 @@ void DiscoveryHandlers::handle_app_depends_on(const httplib::Request & req, http } } +void DiscoveryHandlers::handle_app_is_located_on(const httplib::Request & req, httplib::Response & res) { + try { + if (req.matches.size() < 2) { + HandlerContext::send_error(res, 400, ERR_INVALID_REQUEST, "Invalid request"); + return; + } + + std::string app_id = req.matches[1]; + + auto validation_result = ctx_.validate_entity_id(app_id); + if (!validation_result) { + HandlerContext::send_error(res, 400, ERR_INVALID_PARAMETER, "Invalid app ID", + {{"details", validation_result.error()}, {"app_id", app_id}}); + return; + } + + auto discovery = ctx_.node()->get_discovery_manager(); + auto app_opt = discovery->get_app(app_id); + + if (!app_opt) { + HandlerContext::send_error(res, 404, ERR_ENTITY_NOT_FOUND, "App not found", {{"app_id", app_id}}); + return; + } + + json items = json::array(); + const auto & app = *app_opt; + + if (!app.component_id.empty()) { + auto component_opt = discovery->get_component(app.component_id); + if (component_opt) { + json item; + item["id"] = component_opt->id; + item["name"] = component_opt->name.empty() ? component_opt->id : component_opt->name; + item["href"] = "/api/v1/components/" + component_opt->id; + items.push_back(item); + } else { + RCLCPP_WARN(HandlerContext::logger(), "App '%s' references unknown component '%s'", app_id.c_str(), + app.component_id.c_str()); + } + } + + json response; + response["items"] = items; + + XMedkit resp_ext; + resp_ext.add("total_count", items.size()); + response["x-medkit"] = resp_ext.build(); + + json links; + links["self"] = "/api/v1/apps/" + app_id + "/is-located-on"; + links["app"] = "/api/v1/apps/" + app_id; + response["_links"] = links; + + HandlerContext::send_json(res, response); + } catch (const std::exception & e) { + HandlerContext::send_error(res, 500, ERR_INTERNAL_ERROR, "Internal server error", {{"details", e.what()}}); + RCLCPP_ERROR(HandlerContext::logger(), "Error in handle_app_is_located_on: %s", e.what()); + } +} + // ============================================================================= // Function handlers // ============================================================================= diff --git a/src/ros2_medkit_gateway/src/http/rest_server.cpp b/src/ros2_medkit_gateway/src/http/rest_server.cpp index e323e5dc..6723f982 100644 --- a/src/ros2_medkit_gateway/src/http/rest_server.cpp +++ b/src/ros2_medkit_gateway/src/http/rest_server.cpp @@ -805,6 +805,13 @@ void RESTServer::setup_routes() { } if (et_type_str == "apps") { + reg.get(entity_path + "/is-located-on", + [this](auto & req, auto & res) { + discovery_handlers_->handle_app_is_located_on(req, res); + }) + .tag("Discovery") + .summary("Get app host component"); + reg.get(entity_path + "/depends-on", [this](auto & req, auto & res) { discovery_handlers_->handle_app_depends_on(req, res); diff --git a/src/ros2_medkit_gateway/test/test_discovery_handlers.cpp b/src/ros2_medkit_gateway/test/test_discovery_handlers.cpp index 2d8b31cb..21812bce 100644 --- a/src/ros2_medkit_gateway/test/test_discovery_handlers.cpp +++ b/src/ros2_medkit_gateway/test/test_discovery_handlers.cpp @@ -82,6 +82,9 @@ manifest_version: "1.0" name: "Mapper" is_located_on: "lidar_unit" depends_on: ["planner", "ghost_app"] + - id: "standalone" + name: "Standalone" + description: "Standalone app without a hosting component" functions: - id: "navigation" name: "Navigation" @@ -229,7 +232,7 @@ class DiscoveryHandlersFixtureTest : public ::testing::Test { auto apps = suite_node_->get_discovery_manager()->discover_apps(); auto functions = suite_node_->get_discovery_manager()->discover_functions(); - ASSERT_EQ(apps.size(), 2u); + ASSERT_EQ(apps.size(), 3u); apps[0].is_online = true; apps[0].bound_fqn = "/vehicle/main_ecu/planner"; apps[1].bound_fqn = "/sensors/lidar_unit/mapper"; @@ -566,7 +569,7 @@ TEST_F(DiscoveryHandlersFixtureTest, ListAppsReturnsSeededMetadata) { handlers_->handle_list_apps(req, res); auto body = parse_json(res); - ASSERT_EQ(body["items"].size(), 2); + ASSERT_EQ(body["items"].size(), 3); EXPECT_EQ(body["items"][0]["id"], "planner"); EXPECT_EQ(body["items"][0]["x-medkit"]["component_id"], "main_ecu"); EXPECT_EQ(body["items"][0]["x-medkit"]["is_online"], true); @@ -601,6 +604,57 @@ TEST_F(DiscoveryHandlersFixtureTest, GetAppReturnsLinksAndCapabilities) { EXPECT_EQ(body["x-medkit"]["is_online"], false); } +// @verifies REQ_INTEROP_003 +TEST_F(DiscoveryHandlersFixtureTest, AppIsLocatedOnReturnsParentComponent) { + auto req = make_request_with_match("/api/v1/apps/mapper/is-located-on", R"(/api/v1/apps/([^/]+)/is-located-on)"); + httplib::Response res; + + handlers_->handle_app_is_located_on(req, res); + + auto body = parse_json(res); + ASSERT_EQ(body["items"].size(), 1); + EXPECT_EQ(body["items"][0]["id"], "lidar_unit"); + EXPECT_EQ(body["items"][0]["name"], "Lidar Unit"); + EXPECT_EQ(body["items"][0]["href"], "/api/v1/components/lidar_unit"); + EXPECT_EQ(body["x-medkit"]["total_count"], 1); + EXPECT_EQ(body["_links"]["self"], "/api/v1/apps/mapper/is-located-on"); + EXPECT_EQ(body["_links"]["app"], "/api/v1/apps/mapper"); +} + +// @verifies REQ_INTEROP_003 +TEST_F(DiscoveryHandlersFixtureTest, AppIsLocatedOnReturnsEmptyWhenAppHasNoComponent) { + auto req = make_request_with_match("/api/v1/apps/standalone/is-located-on", R"(/api/v1/apps/([^/]+)/is-located-on)"); + httplib::Response res; + + handlers_->handle_app_is_located_on(req, res); + + auto body = parse_json(res); + ASSERT_TRUE(body["items"].empty()); + EXPECT_EQ(body["x-medkit"]["total_count"], 0); + EXPECT_EQ(body["_links"]["self"], "/api/v1/apps/standalone/is-located-on"); + EXPECT_EQ(body["_links"]["app"], "/api/v1/apps/standalone"); +} + +// @verifies REQ_INTEROP_003 +TEST_F(DiscoveryHandlersValidationTest, AppIsLocatedOnInvalidIdReturns400) { + auto req = make_request_with_match("/api/v1/apps/bad/id/is-located-on", R"(/api/v1/apps/(.+)/is-located-on)"); + httplib::Response res; + + handlers_.handle_app_is_located_on(req, res); + + EXPECT_EQ(res.status, 400); +} + +// @verifies REQ_INTEROP_003 +TEST_F(DiscoveryHandlersFixtureTest, AppIsLocatedOnUnknownAppReturns404) { + auto req = make_request_with_match("/api/v1/apps/unknown/is-located-on", R"(/api/v1/apps/([^/]+)/is-located-on)"); + httplib::Response res; + + handlers_->handle_app_is_located_on(req, res); + + EXPECT_EQ(res.status, 404); +} + // @verifies REQ_INTEROP_009 TEST_F(DiscoveryHandlersFixtureTest, AppDependsOnReturnsResolvedAndMissingDependencies) { auto req = make_request_with_match("/api/v1/apps/mapper/depends-on", R"(/api/v1/apps/([^/]+)/depends-on)"); diff --git a/src/ros2_medkit_gateway/test/test_gateway_node.cpp b/src/ros2_medkit_gateway/test/test_gateway_node.cpp index 1d3f0b6c..fbc6d4db 100644 --- a/src/ros2_medkit_gateway/test/test_gateway_node.cpp +++ b/src/ros2_medkit_gateway/test/test_gateway_node.cpp @@ -157,6 +157,41 @@ class TestGatewayNode : public ::testing::Test { cache.update_functions({func}); } + void load_app_location_fixture() { + const std::string manifest = R"( +manifest_version: "1.0" +areas: + - id: "vehicle" + name: "Vehicle" +components: + - id: "main_ecu" + name: "Main ECU" + area: "vehicle" + namespace: "/vehicle" +apps: + - id: "planner" + name: "Planner" + is_located_on: "main_ecu" + - id: "standalone" + name: "Standalone" +)"; + + ros2_medkit_gateway::DiscoveryConfig config; + config.mode = ros2_medkit_gateway::DiscoveryMode::MANIFEST_ONLY; + config.manifest_path = write_temp_manifest(manifest); + config.manifest_strict_validation = false; + + ASSERT_TRUE(node_->get_discovery_manager()->initialize(config)); + + auto areas = node_->get_discovery_manager()->discover_areas(); + auto components = node_->get_discovery_manager()->discover_components(); + auto apps = node_->get_discovery_manager()->discover_apps(); + auto functions = node_->get_discovery_manager()->discover_functions(); + + auto & cache = const_cast(node_->get_thread_safe_cache()); + cache.update_all(areas, components, apps, functions); + } + std::shared_ptr node_; std::string server_host_; int server_port_; @@ -801,6 +836,23 @@ TEST_F(TestGatewayNode, test_invalid_app_id_bad_request) { EXPECT_EQ(res->status, 400); } +TEST_F(TestGatewayNode, test_app_is_located_on_endpoint) { + load_app_location_fixture(); + + auto client = create_client(); + auto res = client.Get((std::string(API_BASE_PATH) + "/apps/planner/is-located-on").c_str()); + + ASSERT_TRUE(res); + EXPECT_EQ(res->status, 200); + + auto json_response = nlohmann::json::parse(res->body); + ASSERT_TRUE(json_response.contains("items")); + ASSERT_EQ(json_response["items"].size(), 1); + EXPECT_EQ(json_response["items"][0]["id"], "main_ecu"); + EXPECT_EQ(json_response["_links"]["self"], "/api/v1/apps/planner/is-located-on"); + EXPECT_EQ(json_response["_links"]["app"], "/api/v1/apps/planner"); +} + TEST_F(TestGatewayNode, test_invalid_function_id_bad_request) { auto client = create_client(); From bbb9b36899860c6cbba29c8fa42eae046df46a90 Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Sun, 22 Mar 2026 02:09:13 +0900 Subject: [PATCH 2/3] Address review feedback for app host route --- docs/api/rest.rst | 33 ++++++++++++++ .../src/http/handlers/discovery_handlers.cpp | 21 ++++++++- .../test/test_discovery_handlers.cpp | 30 +++++++++++++ .../test/features/test_hateoas.test.py | 43 ++++++++++++++++++- .../test/features/test_health.test.py | 1 + 5 files changed, 124 insertions(+), 4 deletions(-) diff --git a/docs/api/rest.rst b/docs/api/rest.rst index 2fd0d498..dbc2dc6f 100644 --- a/docs/api/rest.rst +++ b/docs/api/rest.rst @@ -157,6 +157,39 @@ Apps ``GET /api/v1/apps/{app_id}`` Get capabilities for a single discovered app. +``GET /api/v1/apps/{app_id}/is-located-on`` + Return the host component reference for an app. + + The response follows the standard ``items`` wrapper and returns: + + - ``0`` items when the app has no associated host component + - ``1`` item when the host component is resolved + - ``1`` item with ``x-medkit.missing=true`` when the app references a host + component that cannot currently be resolved + + **Example Response:** + + .. code-block:: json + + { + "items": [ + { + "id": "temp-sensor-hw", + "name": "Temperature Sensor", + "href": "/api/v1/components/temp-sensor-hw" + } + ], + "x-medkit": { + "total_count": 1 + }, + "_links": { + "self": "/api/v1/apps/engine-temp-sensor/is-located-on", + "app": "/api/v1/apps/engine-temp-sensor" + } + } + + Unknown apps return ``404 App not found`` with ``parameters.app_id``. + Functions ~~~~~~~~~ diff --git a/src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp b/src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp index ecb43e88..021e4dd3 100644 --- a/src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp +++ b/src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp @@ -967,8 +967,12 @@ void DiscoveryHandlers::handle_app_is_located_on(const httplib::Request & req, h return; } + const auto & cache = ctx_.node()->get_thread_safe_cache(); auto discovery = ctx_.node()->get_discovery_manager(); - auto app_opt = discovery->get_app(app_id); + auto app_opt = cache.get_app(app_id); + if (!app_opt) { + app_opt = discovery->get_app(app_id); + } if (!app_opt) { HandlerContext::send_error(res, 404, ERR_ENTITY_NOT_FOUND, "App not found", {{"app_id", app_id}}); @@ -979,7 +983,10 @@ void DiscoveryHandlers::handle_app_is_located_on(const httplib::Request & req, h const auto & app = *app_opt; if (!app.component_id.empty()) { - auto component_opt = discovery->get_component(app.component_id); + auto component_opt = cache.get_component(app.component_id); + if (!component_opt) { + component_opt = discovery->get_component(app.component_id); + } if (component_opt) { json item; item["id"] = component_opt->id; @@ -987,6 +994,16 @@ void DiscoveryHandlers::handle_app_is_located_on(const httplib::Request & req, h item["href"] = "/api/v1/components/" + component_opt->id; items.push_back(item); } else { + json item; + item["id"] = app.component_id; + item["name"] = app.component_id; + item["href"] = "/api/v1/components/" + app.component_id; + + XMedkit ext; + ext.add("missing", true); + item["x-medkit"] = ext.build(); + items.push_back(item); + RCLCPP_WARN(HandlerContext::logger(), "App '%s' references unknown component '%s'", app_id.c_str(), app.component_id.c_str()); } diff --git a/src/ros2_medkit_gateway/test/test_discovery_handlers.cpp b/src/ros2_medkit_gateway/test/test_discovery_handlers.cpp index 21812bce..dfde7fa3 100644 --- a/src/ros2_medkit_gateway/test/test_discovery_handlers.cpp +++ b/src/ros2_medkit_gateway/test/test_discovery_handlers.cpp @@ -635,6 +635,36 @@ TEST_F(DiscoveryHandlersFixtureTest, AppIsLocatedOnReturnsEmptyWhenAppHasNoCompo EXPECT_EQ(body["_links"]["app"], "/api/v1/apps/standalone"); } +// @verifies REQ_INTEROP_003 +TEST_F(DiscoveryHandlersFixtureTest, AppIsLocatedOnReturnsMissingItemWhenHostComponentUnresolved) { + auto & cache = const_cast(suite_node_->get_thread_safe_cache()); + auto apps = cache.get_apps(); + ASSERT_FALSE(apps.empty()); + + bool updated = false; + for (auto & app : apps) { + if (app.id == "mapper") { + app.component_id = "ghost_component"; + updated = true; + break; + } + } + ASSERT_TRUE(updated); + cache.update_apps(apps); + + auto req = make_request_with_match("/api/v1/apps/mapper/is-located-on", R"(/api/v1/apps/([^/]+)/is-located-on)"); + httplib::Response res; + + handlers_->handle_app_is_located_on(req, res); + + auto body = parse_json(res); + ASSERT_EQ(body["items"].size(), 1); + EXPECT_EQ(body["items"][0]["id"], "ghost_component"); + EXPECT_EQ(body["items"][0]["name"], "ghost_component"); + EXPECT_EQ(body["items"][0]["href"], "/api/v1/components/ghost_component"); + EXPECT_EQ(body["items"][0]["x-medkit"]["missing"], true); +} + // @verifies REQ_INTEROP_003 TEST_F(DiscoveryHandlersValidationTest, AppIsLocatedOnInvalidIdReturns400) { auto req = make_request_with_match("/api/v1/apps/bad/id/is-located-on", R"(/api/v1/apps/(.+)/is-located-on)"); diff --git a/src/ros2_medkit_integration_tests/test/features/test_hateoas.test.py b/src/ros2_medkit_integration_tests/test/features/test_hateoas.test.py index ac2238fc..be909326 100644 --- a/src/ros2_medkit_integration_tests/test/features/test_hateoas.test.py +++ b/src/ros2_medkit_integration_tests/test/features/test_hateoas.test.py @@ -16,8 +16,8 @@ """Feature tests for HATEOAS compliance (hrefs, capability URIs, links). Validates that list responses include href fields, entity details include -capability URIs, subareas/subcomponents/contains/hosts/depends-on endpoints -return proper link structures, and x-medkit extensions are present. +capability URIs, subareas/subcomponents/contains/hosts/depends-on/is-located-on +endpoints return proper link structures, and x-medkit extensions are present. """ @@ -416,6 +416,45 @@ def test_depends_on_apps_nonexistent(self): self.assertIn('app_id', data['parameters']) self.assertEqual(data['parameters'].get('app_id'), 'nonexistent_app') + def test_is_located_on_apps_has_href(self): + """GET /apps/{id}/is-located-on returns 0-or-1 component hrefs.""" + response = requests.get( + f'{self.BASE_URL}/apps/temp_sensor/is-located-on', + timeout=10 + ) + self.assertEqual(response.status_code, 200) + + data = response.json() + self.assertIn('items', data) + self.assertIn('_links', data) + self.assertEqual(data['_links']['self'], '/api/v1/apps/temp_sensor/is-located-on') + self.assertEqual(data['_links']['app'], '/api/v1/apps/temp_sensor') + self.assertLessEqual(len(data['items']), 1) + + if data['items']: + host = data['items'][0] + self.assertIn('id', host, "Host component should have 'id'") + self.assertIn('name', host, "Host component should have 'name'") + self.assertIn('href', host, "Host component should have 'href'") + self.assertTrue( + host['href'].startswith('/api/v1/components/'), + f"href should start with /api/v1/components/, got: {host['href']}" + ) + + def test_is_located_on_apps_nonexistent(self): + """GET /apps/{id}/is-located-on returns 404 for unknown app.""" + response = requests.get( + f'{self.BASE_URL}/apps/nonexistent_app/is-located-on', + timeout=10 + ) + self.assertEqual(response.status_code, 404) + + data = response.json() + self.assertIn('error_code', data) + self.assertEqual(data['message'], 'App not found') + self.assertIn('parameters', data) + self.assertEqual(data['parameters'].get('app_id'), 'nonexistent_app') + # ------------------------------------------------------------------ # Functions (test_81) # ------------------------------------------------------------------ diff --git a/src/ros2_medkit_integration_tests/test/features/test_health.test.py b/src/ros2_medkit_integration_tests/test/features/test_health.test.py index 041afe95..3f2e34aa 100644 --- a/src/ros2_medkit_integration_tests/test/features/test_health.test.py +++ b/src/ros2_medkit_integration_tests/test/features/test_health.test.py @@ -122,6 +122,7 @@ def test_root_includes_apps_endpoints(self): endpoints = data['endpoints'] self.assertIn('GET /api/v1/apps', endpoints) self.assertIn('GET /api/v1/apps/{app_id}', endpoints) + self.assertIn('GET /api/v1/apps/{app_id}/is-located-on', endpoints) self.assertIn('GET /api/v1/apps/{app_id}/depends-on', endpoints) self.assertIn('GET /api/v1/apps/{app_id}/data', endpoints) self.assertIn('GET /api/v1/apps/{app_id}/operations', endpoints) From cb03788f27c225c42985070bd6dda201088ca064 Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Sun, 22 Mar 2026 02:33:28 +0900 Subject: [PATCH 3/3] Align app host tests with requirement tags --- docs/api/rest.rst | 2 +- docs/requirements/specs/discovery.rst | 7 +++++++ .../test/test_discovery_handlers.cpp | 10 +++++----- src/ros2_medkit_gateway/test/test_gateway_node.cpp | 1 + .../test/features/test_hateoas.test.py | 2 ++ 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/api/rest.rst b/docs/api/rest.rst index dbc2dc6f..5a16ac79 100644 --- a/docs/api/rest.rst +++ b/docs/api/rest.rst @@ -158,7 +158,7 @@ Apps Get capabilities for a single discovered app. ``GET /api/v1/apps/{app_id}/is-located-on`` - Return the host component reference for an app. + Return the parent component that hosts this app. The response follows the standard ``items`` wrapper and returns: diff --git a/docs/requirements/specs/discovery.rst b/docs/requirements/specs/discovery.rst index e60ae818..474d7909 100644 --- a/docs/requirements/specs/discovery.rst +++ b/docs/requirements/specs/discovery.rst @@ -99,6 +99,13 @@ Discovery Beacon hints shall follow an ACTIVE, STALE, EXPIRED lifecycle based on configurable TTL and expiry durations. +.. req:: GET /apps/{id}/is-located-on + :id: REQ_INTEROP_105 + :status: verified + :tags: Discovery + + The endpoint shall return the component that hosts the addressed application. + .. req:: Vendor endpoints expose beacon data :id: REQ_DISCO_BEACON_04 :status: verified diff --git a/src/ros2_medkit_gateway/test/test_discovery_handlers.cpp b/src/ros2_medkit_gateway/test/test_discovery_handlers.cpp index dfde7fa3..3f0ccedf 100644 --- a/src/ros2_medkit_gateway/test/test_discovery_handlers.cpp +++ b/src/ros2_medkit_gateway/test/test_discovery_handlers.cpp @@ -604,7 +604,7 @@ TEST_F(DiscoveryHandlersFixtureTest, GetAppReturnsLinksAndCapabilities) { EXPECT_EQ(body["x-medkit"]["is_online"], false); } -// @verifies REQ_INTEROP_003 +// @verifies REQ_INTEROP_105 TEST_F(DiscoveryHandlersFixtureTest, AppIsLocatedOnReturnsParentComponent) { auto req = make_request_with_match("/api/v1/apps/mapper/is-located-on", R"(/api/v1/apps/([^/]+)/is-located-on)"); httplib::Response res; @@ -621,7 +621,7 @@ TEST_F(DiscoveryHandlersFixtureTest, AppIsLocatedOnReturnsParentComponent) { EXPECT_EQ(body["_links"]["app"], "/api/v1/apps/mapper"); } -// @verifies REQ_INTEROP_003 +// @verifies REQ_INTEROP_105 TEST_F(DiscoveryHandlersFixtureTest, AppIsLocatedOnReturnsEmptyWhenAppHasNoComponent) { auto req = make_request_with_match("/api/v1/apps/standalone/is-located-on", R"(/api/v1/apps/([^/]+)/is-located-on)"); httplib::Response res; @@ -635,7 +635,7 @@ TEST_F(DiscoveryHandlersFixtureTest, AppIsLocatedOnReturnsEmptyWhenAppHasNoCompo EXPECT_EQ(body["_links"]["app"], "/api/v1/apps/standalone"); } -// @verifies REQ_INTEROP_003 +// @verifies REQ_INTEROP_105 TEST_F(DiscoveryHandlersFixtureTest, AppIsLocatedOnReturnsMissingItemWhenHostComponentUnresolved) { auto & cache = const_cast(suite_node_->get_thread_safe_cache()); auto apps = cache.get_apps(); @@ -665,7 +665,7 @@ TEST_F(DiscoveryHandlersFixtureTest, AppIsLocatedOnReturnsMissingItemWhenHostCom EXPECT_EQ(body["items"][0]["x-medkit"]["missing"], true); } -// @verifies REQ_INTEROP_003 +// @verifies REQ_INTEROP_105 TEST_F(DiscoveryHandlersValidationTest, AppIsLocatedOnInvalidIdReturns400) { auto req = make_request_with_match("/api/v1/apps/bad/id/is-located-on", R"(/api/v1/apps/(.+)/is-located-on)"); httplib::Response res; @@ -675,7 +675,7 @@ TEST_F(DiscoveryHandlersValidationTest, AppIsLocatedOnInvalidIdReturns400) { EXPECT_EQ(res.status, 400); } -// @verifies REQ_INTEROP_003 +// @verifies REQ_INTEROP_105 TEST_F(DiscoveryHandlersFixtureTest, AppIsLocatedOnUnknownAppReturns404) { auto req = make_request_with_match("/api/v1/apps/unknown/is-located-on", R"(/api/v1/apps/([^/]+)/is-located-on)"); httplib::Response res; diff --git a/src/ros2_medkit_gateway/test/test_gateway_node.cpp b/src/ros2_medkit_gateway/test/test_gateway_node.cpp index fbc6d4db..5588a949 100644 --- a/src/ros2_medkit_gateway/test/test_gateway_node.cpp +++ b/src/ros2_medkit_gateway/test/test_gateway_node.cpp @@ -836,6 +836,7 @@ TEST_F(TestGatewayNode, test_invalid_app_id_bad_request) { EXPECT_EQ(res->status, 400); } +// @verifies REQ_INTEROP_105 TEST_F(TestGatewayNode, test_app_is_located_on_endpoint) { load_app_location_fixture(); diff --git a/src/ros2_medkit_integration_tests/test/features/test_hateoas.test.py b/src/ros2_medkit_integration_tests/test/features/test_hateoas.test.py index be909326..b9cbf519 100644 --- a/src/ros2_medkit_integration_tests/test/features/test_hateoas.test.py +++ b/src/ros2_medkit_integration_tests/test/features/test_hateoas.test.py @@ -418,6 +418,7 @@ def test_depends_on_apps_nonexistent(self): def test_is_located_on_apps_has_href(self): """GET /apps/{id}/is-located-on returns 0-or-1 component hrefs.""" + # @verifies REQ_INTEROP_105 response = requests.get( f'{self.BASE_URL}/apps/temp_sensor/is-located-on', timeout=10 @@ -443,6 +444,7 @@ def test_is_located_on_apps_has_href(self): def test_is_located_on_apps_nonexistent(self): """GET /apps/{id}/is-located-on returns 404 for unknown app.""" + # @verifies REQ_INTEROP_105 response = requests.get( f'{self.BASE_URL}/apps/nonexistent_app/is-located-on', timeout=10