This is an automated email from the ASF dual-hosted git repository.

yasith pushed a commit to branch AIRAVATA-3981/integration-health-check
in repository https://gitbox.apache.org/repos/asf/airavata.git

commit 5fb7d4ae6cf7328dff968b9a27775cc3cd379c6f
Author: yasithdev <[email protected]>
AuthorDate: Thu Mar 26 15:35:08 2026 -0500

    feat: add ServiceHealthEndpoint and BackgroundServiceHealth integration 
tests
---
 .../integration/BackgroundServiceHealthTest.java   | 120 ++++++++++++++++++++
 .../integration/ServiceHealthEndpointTest.java     | 122 +++++++++++++++++++++
 2 files changed, 242 insertions(+)

diff --git 
a/integration-tests/src/test/java/org/apache/airavata/integration/BackgroundServiceHealthTest.java
 
b/integration-tests/src/test/java/org/apache/airavata/integration/BackgroundServiceHealthTest.java
new file mode 100644
index 0000000000..14d0aff42d
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/airavata/integration/BackgroundServiceHealthTest.java
@@ -0,0 +1,120 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.airavata.integration;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Verifies that each expected background service is registered and reported
+ * by the {@code /health/services} endpoint.
+ *
+ * <p>The parameterized test asserts that every listed service name appears in
+ * the health response. Services are expected to match the labels used in
+ * {@code AiravataServer#startBackgroundServices()}.
+ */
+@Tag("integration")
+class BackgroundServiceHealthTest {
+
+    private static final String HOST =
+            System.getProperty("airavata.monitoring.host", "localhost");
+    private static final int PORT =
+            Integer.parseInt(System.getProperty("airavata.monitoring.port", 
"9097"));
+
+    private static final String HEALTH_URL = "http://"; + HOST + ":" + PORT + 
"/health/services";
+
+    private static HttpClient httpClient;
+    private static ObjectMapper objectMapper;
+    private static JsonNode cachedRoot;
+
+    @BeforeAll
+    static void fetchHealthResponse() throws Exception {
+        httpClient = HttpClient.newBuilder()
+                .connectTimeout(Duration.ofSeconds(10))
+                .build();
+        objectMapper = new ObjectMapper();
+
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(HEALTH_URL))
+                .timeout(Duration.ofSeconds(10))
+                .GET()
+                .build();
+        HttpResponse<String> response = httpClient.send(request, 
HttpResponse.BodyHandlers.ofString());
+        assertEquals(200, response.statusCode(),
+                "Could not reach /health/services — got HTTP " + 
response.statusCode());
+        cachedRoot = objectMapper.readTree(response.body());
+    }
+
+    static Stream<String> expectedServices() {
+        return Stream.of(
+                "db_event_manager",
+                "monitoring_server",
+                "helix_controller",
+                "helix_participant",
+                "pre_workflow_manager",
+                "post_workflow_manager",
+                "parser_workflow_manager",
+                "realtime_monitor");
+    }
+
+    @ParameterizedTest(name = "{0} is registered")
+    @MethodSource("expectedServices")
+    void serviceShouldBeRegistered(String serviceName) {
+        assertNotNull(cachedRoot, "/health/services response was not fetched");
+        assertTrue(cachedRoot.has(serviceName),
+                "Expected service '" + serviceName + "' to be registered, "
+                        + "but it was not found in the health response. "
+                        + "Registered services: " + cachedRoot.fieldNames());
+    }
+
+    @ParameterizedTest(name = "{0} is UP with positive uptime")
+    @MethodSource("expectedServices")
+    void registeredServiceShouldBeUp(String serviceName) {
+        assertNotNull(cachedRoot, "/health/services response was not fetched");
+
+        JsonNode serviceNode = cachedRoot.get(serviceName);
+        assertNotNull(serviceNode, "Service '" + serviceName + "' is not in 
the health response");
+
+        JsonNode statusNode = serviceNode.get("status");
+        assertNotNull(statusNode, "Service '" + serviceName + "' has no 
'status' field");
+        assertEquals("UP", statusNode.asText(),
+                "Service '" + serviceName + "' is not UP — status: " + 
statusNode.asText());
+
+        JsonNode uptimeNode = serviceNode.get("uptimeMs");
+        assertNotNull(uptimeNode, "Service '" + serviceName + "' has no 
'uptimeMs' field");
+        assertTrue(uptimeNode.asLong() > 0,
+                "Service '" + serviceName + "' is UP but uptimeMs is not 
positive: "
+                        + uptimeNode.asLong());
+    }
+}
diff --git 
a/integration-tests/src/test/java/org/apache/airavata/integration/ServiceHealthEndpointTest.java
 
b/integration-tests/src/test/java/org/apache/airavata/integration/ServiceHealthEndpointTest.java
new file mode 100644
index 0000000000..219ff00d35
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/airavata/integration/ServiceHealthEndpointTest.java
@@ -0,0 +1,122 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.airavata.integration;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.Iterator;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies the {@code /health/services} endpoint on the MonitoringServer.
+ *
+ * <p>The endpoint returns a JSON object whose keys are service names and 
values are
+ * {@code ServiceStatus} objects with fields: {@code status} (UP/DOWN),
+ * {@code uptimeMs} (long), and {@code lastError} (string or null).
+ *
+ * <p>Every service that is UP must report a positive uptimeMs.
+ */
+@Tag("integration")
+class ServiceHealthEndpointTest {
+
+    private static final String HOST =
+            System.getProperty("airavata.monitoring.host", "localhost");
+    private static final int PORT =
+            Integer.parseInt(System.getProperty("airavata.monitoring.port", 
"9097"));
+
+    private static final String HEALTH_URL = "http://"; + HOST + ":" + PORT + 
"/health/services";
+
+    private static HttpClient httpClient;
+    private static ObjectMapper objectMapper;
+
+    @BeforeAll
+    static void setUp() {
+        httpClient = HttpClient.newBuilder()
+                .connectTimeout(Duration.ofSeconds(10))
+                .build();
+        objectMapper = new ObjectMapper();
+    }
+
+    @Test
+    void healthEndpointShouldReturn200() throws Exception {
+        HttpResponse<String> response = getServicesResponse();
+        assertEquals(200, response.statusCode(),
+                "Expected HTTP 200 from /health/services, got " + 
response.statusCode());
+    }
+
+    @Test
+    void healthResponseShouldBeNonEmptyJsonObject() throws Exception {
+        HttpResponse<String> response = getServicesResponse();
+        assertEquals(200, response.statusCode());
+
+        JsonNode root = objectMapper.readTree(response.body());
+        assertNotNull(root, "Response body could not be parsed as JSON");
+        assertTrue(root.isObject(), "Expected JSON object at root, got: " + 
root.getNodeType());
+        assertFalse(root.isEmpty(), "Expected at least one service entry in 
the response");
+    }
+
+    @Test
+    void allUpServicesShouldHavePositiveUptime() throws Exception {
+        HttpResponse<String> response = getServicesResponse();
+        assertEquals(200, response.statusCode());
+
+        JsonNode root = objectMapper.readTree(response.body());
+        Iterator<Map.Entry<String, JsonNode>> fields = root.fields();
+        while (fields.hasNext()) {
+            Map.Entry<String, JsonNode> entry = fields.next();
+            String serviceName = entry.getKey();
+            JsonNode serviceNode = entry.getValue();
+
+            JsonNode statusNode = serviceNode.get("status");
+            assertNotNull(statusNode, "Service '" + serviceName + "' is 
missing 'status' field");
+
+            if ("UP".equals(statusNode.asText())) {
+                JsonNode uptimeNode = serviceNode.get("uptimeMs");
+                assertNotNull(uptimeNode,
+                        "UP service '" + serviceName + "' is missing 
'uptimeMs' field");
+                assertTrue(uptimeNode.asLong() > 0,
+                        "UP service '" + serviceName + "' has non-positive 
uptimeMs: "
+                                + uptimeNode.asLong());
+            }
+        }
+    }
+
+    private HttpResponse<String> getServicesResponse() throws Exception {
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(HEALTH_URL))
+                .timeout(Duration.ofSeconds(10))
+                .GET()
+                .build();
+        return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+    }
+}

Reply via email to