You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@geode.apache.org by ji...@apache.org on 2026/08/26 14:37:29 UTC
(geode) branch develop updated: GEODE-10612: Encode region path in Pulse region-detail error messages (#8041)
This is an automated email from the ASF dual-hosted git repository.
JinwooHwang pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git
The following commit(s) were added to refs/heads/develop by this push:
new 2470c4e891 GEODE-10612: Encode region path in Pulse region-detail error messages (#8041)
2470c4e891 is described below
commit 2470c4e891b56e74cdf42fb21e86a4588d88f4e3
Author: Jinwoo Hwang <92...@users.noreply.github.com>
AuthorDate: Wed Aug 26 10:37:23 2026 -0400
GEODE-10612: Encode region path in Pulse region-detail error messages (#8041)
When a region-detail request names a path that does not resolve, the
region services return "Region [<path>] is not available" in the
errorOnRegion field, which the Pulse UI displays. Paths containing
characters such as '<' or '&' did not display correctly.
---
.../RegionDetailErrorMessageIntegrationTest.java | 155 +++++++++++++++++++++
.../service/ClusterSelectedRegionService.java | 5 +-
.../ClusterSelectedRegionsMemberService.java | 5 +-
.../service/RegionErrorMessageEncodingTest.java | 132 ++++++++++++++++++
4 files changed, 295 insertions(+), 2 deletions(-)
diff --git a/geode-pulse/src/integrationTest/java/org/apache/geode/tools/pulse/controllers/RegionDetailErrorMessageIntegrationTest.java b/geode-pulse/src/integrationTest/java/org/apache/geode/tools/pulse/controllers/RegionDetailErrorMessageIntegrationTest.java
new file mode 100644
index 0000000000..94aa800e6e
--- /dev/null
+++ b/geode-pulse/src/integrationTest/java/org/apache/geode/tools/pulse/controllers/RegionDetailErrorMessageIntegrationTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.geode.tools.pulse.controllers;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+import static org.mockito.quality.Strictness.LENIENT;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+import static org.springframework.http.MediaType.parseMediaType;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.security.Principal;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import org.apache.geode.test.junit.categories.PulseTest;
+import org.apache.geode.tools.pulse.internal.data.Cluster;
+import org.apache.geode.tools.pulse.internal.data.Repository;
+
+/**
+ * Covers the region-detail error message end to end, from the {@code /pulseUpdate} request the
+ * Pulse UI posts through to the JSON it receives back.
+ */
+@Category({PulseTest.class})
+@RunWith(SpringJUnit4ClassRunner.class)
+@WebAppConfiguration
+@ContextConfiguration("classpath*:WEB-INF/pulse-servlet.xml")
+@ActiveProfiles({"pulse.controller.test"})
+public class RegionDetailErrorMessageIntegrationTest {
+
+ private static final String PATH_WITH_SPECIAL_CHARACTERS = "/orders<2026>&archive";
+ private static final String ENCODED_MESSAGE =
+ "Region [/orders<2026>&archive] is not available";
+
+ private static final MediaType JSON_MEDIA_TYPE = parseMediaType(APPLICATION_JSON_VALUE);
+ private static final Principal PRINCIPAL = () -> "test-user";
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ @Rule
+ public MockitoRule mockitoRule = MockitoJUnit.rule().strictness(LENIENT);
+
+ @Autowired
+ private WebApplicationContext wac;
+
+ @Autowired
+ private Repository repository;
+
+ @Mock
+ Cluster cluster;
+
+ private MockMvc mockMvc;
+
+ @Before
+ public void setup() {
+ when(repository.getCluster()).thenReturn(cluster);
+ when(cluster.getServerName()).thenReturn("mock-cluster");
+ // The requested path resolves to no region, so the services take the error branch.
+ when(cluster.getClusterRegion(anyString())).thenReturn(null);
+
+ mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
+ }
+
+ @Test
+ public void pulseUpdateEncodesSpecialCharactersForClusterSelectedRegion() throws Exception {
+ MvcResult result = mockMvc
+ .perform(post("/pulseUpdate")
+ .with(csrf())
+ .param("pulseData", pulseData("ClusterSelectedRegion", PATH_WITH_SPECIAL_CHARACTERS))
+ .principal(PRINCIPAL)
+ .accept(JSON_MEDIA_TYPE))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.ClusterSelectedRegion.selectedRegion.errorOnRegion")
+ .value(ENCODED_MESSAGE))
+ .andReturn();
+
+ assertThat(result.getResponse().getContentAsString())
+ .contains("/orders<2026>&archive");
+ }
+
+ @Test
+ public void pulseUpdateEncodesSpecialCharactersForClusterSelectedRegionsMember()
+ throws Exception {
+ MvcResult result = mockMvc
+ .perform(post("/pulseUpdate")
+ .with(csrf())
+ .param("pulseData",
+ pulseData("ClusterSelectedRegionsMember", PATH_WITH_SPECIAL_CHARACTERS))
+ .principal(PRINCIPAL)
+ .accept(JSON_MEDIA_TYPE))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.ClusterSelectedRegionsMember.selectedRegionsMembers.errorOnRegion")
+ .value(ENCODED_MESSAGE))
+ .andReturn();
+
+ assertThat(result.getResponse().getContentAsString())
+ .contains("/orders<2026>&archive");
+ }
+
+ @Test
+ public void pulseUpdateLeavesOrdinaryRegionPathUnchanged() throws Exception {
+ mockMvc
+ .perform(post("/pulseUpdate")
+ .with(csrf())
+ .param("pulseData", pulseData("ClusterSelectedRegion", "/mock-region"))
+ .principal(PRINCIPAL)
+ .accept(JSON_MEDIA_TYPE))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.ClusterSelectedRegion.selectedRegion.errorOnRegion")
+ .value("Region [/mock-region] is not available"));
+ }
+
+ /** Builds the {@code pulseData} body the Pulse frontend posts for the region-detail page. */
+ private static String pulseData(String service, String regionFullPath) {
+ ObjectNode parameters = MAPPER.createObjectNode();
+ parameters.put("regionFullPath", regionFullPath);
+ ObjectNode root = MAPPER.createObjectNode();
+ root.set(service, parameters);
+ return root.toString();
+ }
+}
diff --git a/geode-pulse/src/main/java/org/apache/geode/tools/pulse/internal/service/ClusterSelectedRegionService.java b/geode-pulse/src/main/java/org/apache/geode/tools/pulse/internal/service/ClusterSelectedRegionService.java
index f99c501550..74f55416ee 100644
--- a/geode-pulse/src/main/java/org/apache/geode/tools/pulse/internal/service/ClusterSelectedRegionService.java
+++ b/geode-pulse/src/main/java/org/apache/geode/tools/pulse/internal/service/ClusterSelectedRegionService.java
@@ -30,6 +30,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.text.StringEscapeUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
@@ -222,7 +223,9 @@ public class ClusterSelectedRegionService implements PulseService {
return regionJSON;
} else {
ObjectNode responseJSON = mapper.createObjectNode();
- responseJSON.put("errorOnRegion", "Region [" + selectedRegionFullPath + "] is not available");
+ responseJSON.put("errorOnRegion",
+ "Region [" + StringEscapeUtils.escapeHtml4(selectedRegionFullPath)
+ + "] is not available");
return responseJSON;
}
}
diff --git a/geode-pulse/src/main/java/org/apache/geode/tools/pulse/internal/service/ClusterSelectedRegionsMemberService.java b/geode-pulse/src/main/java/org/apache/geode/tools/pulse/internal/service/ClusterSelectedRegionsMemberService.java
index 238cdabc89..6d1af501d2 100644
--- a/geode-pulse/src/main/java/org/apache/geode/tools/pulse/internal/service/ClusterSelectedRegionsMemberService.java
+++ b/geode-pulse/src/main/java/org/apache/geode/tools/pulse/internal/service/ClusterSelectedRegionsMemberService.java
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.servlet.http.HttpServletRequest;
+import org.apache.commons.text.StringEscapeUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
@@ -146,7 +147,9 @@ public class ClusterSelectedRegionsMemberService implements PulseService {
return regionMemberJSON;
} else {
ObjectNode responseJSON = mapper.createObjectNode();
- responseJSON.put("errorOnRegion", "Region [" + selectedRegionFullPath + "] is not available");
+ responseJSON.put("errorOnRegion",
+ "Region [" + StringEscapeUtils.escapeHtml4(selectedRegionFullPath)
+ + "] is not available");
return responseJSON;
}
}
diff --git a/geode-pulse/src/test/java/org/apache/geode/tools/pulse/internal/service/RegionErrorMessageEncodingTest.java b/geode-pulse/src/test/java/org/apache/geode/tools/pulse/internal/service/RegionErrorMessageEncodingTest.java
new file mode 100644
index 0000000000..2d1b086744
--- /dev/null
+++ b/geode-pulse/src/test/java/org/apache/geode/tools/pulse/internal/service/RegionErrorMessageEncodingTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.geode.tools.pulse.internal.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.security.Principal;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.servlet.http.HttpServletRequest;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.geode.tools.pulse.internal.data.Cluster;
+import org.apache.geode.tools.pulse.internal.data.Repository;
+
+/**
+ * Tests the {@code errorOnRegion} message the region-detail services produce when the requested
+ * region path does not resolve.
+ *
+ * <p>
+ * Paths containing characters such as {@code <} or {@code &} are encoded so the message displays
+ * as written. Lookup is unaffected and uses the path as supplied.
+ */
+public class RegionErrorMessageEncodingTest {
+
+ private static final String PATH_WITH_SPECIAL_CHARACTERS = "/orders<2026>&archive";
+ private static final String ENCODED_MESSAGE =
+ "Region [/orders<2026>&archive] is not available";
+ private static final String ORDINARY_PATH = "/mock-region";
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private Repository repository;
+ private Cluster cluster;
+ private HttpServletRequest request;
+
+ @Before
+ public void setUp() {
+ repository = mock(Repository.class);
+ cluster = mock(Cluster.class);
+ request = mock(HttpServletRequest.class);
+ Principal principal = mock(Principal.class);
+ when(principal.getName()).thenReturn("admin");
+ when(request.getUserPrincipal()).thenReturn(principal);
+ when(repository.getCluster()).thenReturn(cluster);
+ when(cluster.getServerName()).thenReturn("mock-cluster");
+ // No region resolves, so every call below takes the error branch.
+ when(cluster.getClusterRegion(anyString())).thenReturn(null);
+ }
+
+ @Test
+ public void clusterSelectedRegionEncodesSpecialCharactersInErrorMessage() throws Exception {
+ assertThat(selectedRegionError(PATH_WITH_SPECIAL_CHARACTERS)).isEqualTo(ENCODED_MESSAGE);
+ }
+
+ @Test
+ public void clusterSelectedRegionsMemberEncodesSpecialCharactersInErrorMessage()
+ throws Exception {
+ assertThat(selectedRegionsMemberError(PATH_WITH_SPECIAL_CHARACTERS)).isEqualTo(ENCODED_MESSAGE);
+ }
+
+ @Test
+ public void clusterSelectedRegionLeavesOrdinaryPathUnchanged() throws Exception {
+ assertThat(selectedRegionError(ORDINARY_PATH))
+ .isEqualTo("Region [" + ORDINARY_PATH + "] is not available");
+ }
+
+ @Test
+ public void clusterSelectedRegionsMemberLeavesOrdinaryPathUnchanged() throws Exception {
+ assertThat(selectedRegionsMemberError(ORDINARY_PATH))
+ .isEqualTo("Region [" + ORDINARY_PATH + "] is not available");
+ }
+
+ @Test
+ public void clusterSelectedRegionLooksTheRegionUpByTheSuppliedPath() throws Exception {
+ selectedRegionError(PATH_WITH_SPECIAL_CHARACTERS);
+
+ verify(cluster).getClusterRegion(PATH_WITH_SPECIAL_CHARACTERS);
+ }
+
+ @Test
+ public void clusterSelectedRegionsMemberLooksTheRegionUpByTheSuppliedPath() throws Exception {
+ selectedRegionsMemberError(PATH_WITH_SPECIAL_CHARACTERS);
+
+ verify(cluster).getClusterRegion(PATH_WITH_SPECIAL_CHARACTERS);
+ }
+
+ private String selectedRegionError(String regionFullPath) throws Exception {
+ when(request.getParameter("pulseData"))
+ .thenReturn(pulseData("ClusterSelectedRegion", regionFullPath));
+
+ ObjectNode json = new ClusterSelectedRegionService(repository).execute(request);
+
+ return json.get("selectedRegion").get("errorOnRegion").asText();
+ }
+
+ private String selectedRegionsMemberError(String regionFullPath) throws Exception {
+ when(request.getParameter("pulseData"))
+ .thenReturn(pulseData("ClusterSelectedRegionsMember", regionFullPath));
+
+ ObjectNode json = new ClusterSelectedRegionsMemberService(repository).execute(request);
+
+ return json.get("selectedRegionsMembers").get("errorOnRegion").asText();
+ }
+
+ /** Builds the {@code pulseData} body the Pulse frontend posts for the region-detail page. */
+ private static String pulseData(String service, String regionFullPath) {
+ ObjectNode parameters = MAPPER.createObjectNode();
+ parameters.put("regionFullPath", regionFullPath);
+ ObjectNode root = MAPPER.createObjectNode();
+ root.set(service, parameters);
+ return root.toString();
+ }
+}