diff --git a/openespi-datacustodian/pom.xml b/openespi-datacustodian/pom.xml
index 19b93994..97c2f6bc 100644
--- a/openespi-datacustodian/pom.xml
+++ b/openespi-datacustodian/pom.xml
@@ -360,6 +360,15 @@
+
+
+
+
+ build-info
+
+
+
diff --git a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/AboutController.java b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/AboutController.java
index 7565fd9b..b43d9cfb 100644
--- a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/AboutController.java
+++ b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/AboutController.java
@@ -19,31 +19,63 @@
package org.greenbuttonalliance.espi.datacustodian.web;
-import org.springframework.boot.SpringBootVersion;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.info.BuildProperties;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
- * Public "About" page (#175). Replaces the legacy about.jsp build-info table with a small,
- * non-sensitive summary of the running implementation. Public (footer-linked from every page,
- * including the anonymous login page).
+ * Public "About" page (#175), footer-linked from every page including the anonymous login page.
+ *
+ *
Shows only what identifies the product: name, version, build time, the ESPI standard it
+ * implements and its release status. It deliberately does NOT show the Java or Spring Boot version:
+ * exact framework versions help attackers pick known CVEs, so they stay behind the admin-only
+ * {@code /actuator/info} (#240). The version and build time come from {@link BuildProperties}
+ * ({@code spring-boot-maven-plugin:build-info}), which, unlike the jar manifest, is also present
+ * under {@code spring-boot:run}.
*/
@Controller
public class AboutController {
+ static final String IMPLEMENTATION = "OpenESPI Green Button Data Custodian";
+ static final String ESPI_STANDARD = "NAESB REQ.21 ESPI, version 4.1";
+
+ private static final DateTimeFormatter BUILD_TIME =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm 'UTC'").withZone(ZoneOffset.UTC);
+
+ private final ObjectProvider buildProperties;
+
+ public AboutController(ObjectProvider buildProperties) {
+ this.buildProperties = buildProperties;
+ }
+
@GetMapping("/about")
public String about(Model model) {
+ BuildProperties build = buildProperties.getIfAvailable();
+ String version = build != null ? build.getVersion() : null;
+
Map info = new LinkedHashMap<>();
- String version = AboutController.class.getPackage().getImplementationVersion();
- info.put("Implementation", "OpenESPI Green Button Data Custodian");
+ info.put("Implementation", IMPLEMENTATION);
info.put("Version", version != null ? version : "(development build)");
- info.put("Java version", System.getProperty("java.version", "—"));
- info.put("Spring Boot version", SpringBootVersion.getVersion());
+ if (build != null && build.getTime() != null) {
+ info.put("Build time", BUILD_TIME.format(build.getTime()));
+ }
+ info.put("Standard", ESPI_STANDARD);
+ info.put("Status", releaseStatus(version));
model.addAttribute("info", info);
return "about";
}
+
+ /** A version without a release (null or a SNAPSHOT) is pre-release and not for production use. */
+ static String releaseStatus(String version) {
+ return version == null || version.endsWith("-SNAPSHOT")
+ ? "Pre-release: not for production use"
+ : "Release";
+ }
}
diff --git a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/VersionRESTController.java b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/VersionRESTController.java
deleted file mode 100644
index affff259..00000000
--- a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/VersionRESTController.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- *
- * Copyright (c) 2025-2026 Green Button Alliance, Inc.
- *
- *
- * 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.
- *
- */
-
-package org.greenbuttonalliance.espi.datacustodian.web;
-
-import org.springframework.stereotype.Controller;
-import org.springframework.ui.ModelMap;
-import org.springframework.web.bind.annotation.GetMapping;
-import jakarta.servlet.ServletContext;
-import jakarta.servlet.http.HttpServletRequest;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.jar.Manifest;
-
-@Controller
-// @RequestMapping - DISABLED
-public class VersionRESTController extends BaseController {
-
- /**
- * Handling GET request to retrieve details from MANIFEST.MF file
- *
- * @param request
- * HTTP Servlet Request
- * @param model
- * ModelMap data
- * @throws IOException
- * Exception thrown by failed or interrupted I/O operations.
- *
- * @return implementation details
- */
- @GetMapping("/about-version")
- public String getBuildNumber(HttpServletRequest request, ModelMap model)
- throws IOException {
-
- ServletContext context = request.getSession().getServletContext();
- InputStream manifestStream = context
- .getResourceAsStream("/META-INF/MANIFEST.MF");
- Manifest manifest = new Manifest(manifestStream);
-
- Map aboutInfo = new HashMap<>();
- aboutInfo.put("Implementation-Vendor", manifest.getMainAttributes()
- .getValue("Implementation-Vendor"));
- aboutInfo.put("Implementation-Title", manifest.getMainAttributes()
- .getValue("Implementation-Title"));
- aboutInfo.put("Implementation-Version", manifest.getMainAttributes()
- .getValue("Implementation-Version"));
- aboutInfo.put("Implementation-Jdk", manifest.getMainAttributes()
- .getValue("Build-Jdk"));
- aboutInfo.put("Implementation-Build", manifest.getMainAttributes()
- .getValue("Implementation-Build"));
- aboutInfo.put("Implementation-Build-Time", manifest.getMainAttributes()
- .getValue("Implementation-Build-Time"));
-
- model.put("aboutInfo", aboutInfo);
- return "/about";
-
- }
-
-}
\ No newline at end of file
diff --git a/openespi-datacustodian/src/main/resources/templates/about.html b/openespi-datacustodian/src/main/resources/templates/about.html
index d2449459..2696c45a 100644
--- a/openespi-datacustodian/src/main/resources/templates/about.html
+++ b/openespi-datacustodian/src/main/resources/templates/about.html
@@ -8,25 +8,43 @@
diff --git a/openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/AboutControllerTest.java b/openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/AboutControllerTest.java
new file mode 100644
index 00000000..82730f69
--- /dev/null
+++ b/openespi-datacustodian/src/test/java/org/greenbuttonalliance/espi/datacustodian/web/AboutControllerTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2026 Green Button Alliance, Inc.
+ *
+ * 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
+ *
+ * https://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.greenbuttonalliance.espi.datacustodian.web;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootVersion;
+import org.springframework.boot.info.BuildProperties;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * The About page is public, identifies the product and its release status, and does not disclose
+ * the Java or Spring Boot version to anonymous visitors.
+ */
+@SpringBootTest
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+class AboutControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectProvider buildProperties;
+
+ @Test
+ void about_is_public_and_shows_product_identity_without_framework_versions() throws Exception {
+ String html = mockMvc.perform(get("/about"))
+ .andExpect(status().isOk())
+ .andReturn().getResponse().getContentAsString();
+
+ assertThat(html)
+ .contains(AboutController.IMPLEMENTATION, AboutController.ESPI_STANDARD, "Status", "SECURITY.md")
+ .doesNotContain("Java version", System.getProperty("java.version"))
+ .doesNotContain("Spring Boot version", SpringBootVersion.getVersion());
+ }
+
+ @Test
+ void about_shows_the_build_version_when_build_info_is_present() throws Exception {
+ BuildProperties build = buildProperties.getIfAvailable();
+ String html = mockMvc.perform(get("/about"))
+ .andExpect(status().isOk())
+ .andReturn().getResponse().getContentAsString();
+
+ // build-info.properties is generated by spring-boot-maven-plugin:build-info in a Maven build
+ assertThat(build).as("BuildProperties (spring-boot-maven-plugin:build-info)").isNotNull();
+ assertThat(html).contains(build.getVersion(), AboutController.releaseStatus(build.getVersion()), "Build time");
+ }
+
+ @Test
+ void release_status_follows_the_version() {
+ assertThat(AboutController.releaseStatus("4.0.0-SNAPSHOT")).startsWith("Pre-release");
+ assertThat(AboutController.releaseStatus(null)).startsWith("Pre-release");
+ assertThat(AboutController.releaseStatus("4.0.0")).isEqualTo("Release");
+ }
+}
diff --git a/openespi-thirdparty/pom.xml b/openespi-thirdparty/pom.xml
index 40ff3cc9..f7fb6d8f 100644
--- a/openespi-thirdparty/pom.xml
+++ b/openespi-thirdparty/pom.xml
@@ -357,6 +357,15 @@
+
+
+
+
+ build-info
+
+
+
diff --git a/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/config/SecurityConfiguration.java b/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/config/SecurityConfiguration.java
index aba8ba6d..99ab0937 100644
--- a/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/config/SecurityConfiguration.java
+++ b/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/config/SecurityConfiguration.java
@@ -58,8 +58,9 @@ public class SecurityConfiguration {
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
- // Public pages (the Terms of Service and Usage Policy are linked from the navbar)
- .requestMatchers("/", "/home", "/login", "/TermsOfService", "/UsagePolicy", "/error",
+ // Public pages (Terms of Service and Usage Policy are linked from the navbar, About from
+ // the footer)
+ .requestMatchers("/", "/home", "/login", "/about", "/TermsOfService", "/UsagePolicy", "/error",
"/css/**", "/js/**", "/images/**").permitAll()
// ESPI notification receipt: the Data Custodian POSTs a BatchList here without an
// OAuth token — this endpoint is secured at the transport layer (TLS), not with an
diff --git a/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/web/AboutController.java b/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/web/AboutController.java
new file mode 100644
index 00000000..fed7eb65
--- /dev/null
+++ b/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/web/AboutController.java
@@ -0,0 +1,82 @@
+/*
+ *
+ * Copyright (c) 2026 Green Button Alliance, Inc.
+ *
+ *
+ * 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.
+ *
+ */
+
+package org.greenbuttonalliance.espi.thirdparty.web;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.info.BuildProperties;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Public "About" page, footer-linked from every page. Mirrors the Data Custodian's About page.
+ *
+ *
Shows only what identifies the product: name, version, build time, the ESPI standard it
+ * implements and its release status. It deliberately does NOT show the Java or Spring Boot version:
+ * exact framework versions help attackers pick known CVEs, so they stay behind the admin-only
+ * {@code /actuator/info}. The version and build time come from {@link BuildProperties}
+ * ({@code spring-boot-maven-plugin:build-info}). Replaces the legacy {@code /about-version}
+ * controller, which read a servlet-context manifest that does not exist in a Spring Boot jar and
+ * rendered a template that did not exist.
+ */
+@Controller
+public class AboutController {
+
+ static final String IMPLEMENTATION = "OpenESPI Green Button Third Party";
+ static final String ESPI_STANDARD = "NAESB REQ.21 ESPI, version 4.1";
+
+ private static final DateTimeFormatter BUILD_TIME =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm 'UTC'").withZone(ZoneOffset.UTC);
+
+ private final ObjectProvider buildProperties;
+
+ public AboutController(ObjectProvider buildProperties) {
+ this.buildProperties = buildProperties;
+ }
+
+ @GetMapping("/about")
+ public String about(Model model) {
+ BuildProperties build = buildProperties.getIfAvailable();
+ String version = build != null ? build.getVersion() : null;
+
+ Map info = new LinkedHashMap<>();
+ info.put("Implementation", IMPLEMENTATION);
+ info.put("Version", version != null ? version : "(development build)");
+ if (build != null && build.getTime() != null) {
+ info.put("Build time", BUILD_TIME.format(build.getTime()));
+ }
+ info.put("Standard", ESPI_STANDARD);
+ info.put("Status", releaseStatus(version));
+ model.addAttribute("info", info);
+ return "about";
+ }
+
+ /** A version without a release (null or a SNAPSHOT) is pre-release and not for production use. */
+ static String releaseStatus(String version) {
+ return version == null || version.endsWith("-SNAPSHOT")
+ ? "Pre-release: not for production use"
+ : "Release";
+ }
+}
diff --git a/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/web/VersionRESTController.java b/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/web/VersionRESTController.java
deleted file mode 100644
index f42c62f7..00000000
--- a/openespi-thirdparty/src/main/java/org/greenbuttonalliance/espi/thirdparty/web/VersionRESTController.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- *
- * Copyright (c) 2025-2026 Green Button Alliance, Inc.
- *
- *
- * 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.
- *
- */
-
-package org.greenbuttonalliance.espi.thirdparty.web;
-
-import org.springframework.stereotype.Controller;
-import org.springframework.ui.ModelMap;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-
-import jakarta.servlet.ServletContext;
-import jakarta.servlet.http.HttpServletRequest;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.jar.Manifest;
-
-@Controller
-@RequestMapping
-public class VersionRESTController extends BaseController {
-
- /**
- * Handling GET request to retrieve details from MANIFEST.MF file
- *
- * @return implementation details
- */
- @RequestMapping(value = "/about-version", method = RequestMethod.GET)
- public String getBuildNumber(HttpServletRequest request, ModelMap model)
- throws IOException {
-
- ServletContext context = request.getSession().getServletContext();
- InputStream manifestStream = context
- .getResourceAsStream("/META-INF/MANIFEST.MF");
- Manifest manifest = new Manifest(manifestStream);
-
- Map aboutInfo = new HashMap<>();
- aboutInfo.put("Implementation-Vendor", manifest.getMainAttributes()
- .getValue("Implementation-Vendor"));
- aboutInfo.put("Implementation-Title", manifest.getMainAttributes()
- .getValue("Implementation-Title"));
- aboutInfo.put("Implementation-Version", manifest.getMainAttributes()
- .getValue("Implementation-Version"));
- aboutInfo.put("Implementation-Jdk", manifest.getMainAttributes()
- .getValue("Build-Jdk"));
- aboutInfo.put("Implementation-Build", manifest.getMainAttributes()
- .getValue("Implementation-Build"));
- aboutInfo.put("Implementation-Build-Time", manifest.getMainAttributes()
- .getValue("Implementation-Build-Time"));
-
- model.put("aboutInfo", aboutInfo);
- return "/about";
-
- }
-
-}
\ No newline at end of file
diff --git a/openespi-thirdparty/src/main/resources/templates/about.html b/openespi-thirdparty/src/main/resources/templates/about.html
new file mode 100644
index 00000000..06d7c035
--- /dev/null
+++ b/openespi-thirdparty/src/main/resources/templates/about.html
@@ -0,0 +1,54 @@
+
+
+
+ About - Third Party - Green Button Alliance
+
+
+
+
+
+
+
About
+
+ A reference implementation of the NAESB ESPI (Green Button) Third Party, maintained by the
+ Green Button Alliance.
+
diff --git a/openespi-thirdparty/src/test/java/org/greenbuttonalliance/espi/thirdparty/web/AboutControllerTest.java b/openespi-thirdparty/src/test/java/org/greenbuttonalliance/espi/thirdparty/web/AboutControllerTest.java
new file mode 100644
index 00000000..993efeff
--- /dev/null
+++ b/openespi-thirdparty/src/test/java/org/greenbuttonalliance/espi/thirdparty/web/AboutControllerTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2026 Green Button Alliance, Inc.
+ *
+ * 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
+ *
+ * https://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.greenbuttonalliance.espi.thirdparty.web;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootVersion;
+import org.springframework.boot.info.BuildProperties;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * The Third Party About page is public, rendered through the shared GBA layout, identifies the
+ * product and its release status, and does not disclose the Java or Spring Boot version.
+ */
+@SpringBootTest
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+class AboutControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectProvider buildProperties;
+
+ @Test
+ void about_is_public_and_shows_product_identity_without_framework_versions() throws Exception {
+ String html = mockMvc.perform(get("/about"))
+ .andExpect(status().isOk())
+ .andReturn().getResponse().getContentAsString();
+
+ assertThat(html)
+ .contains(AboutController.IMPLEMENTATION, AboutController.ESPI_STANDARD, "Status", "SECURITY.md")
+ .contains("gba-navbar", "gba-footer", "data-gba-app=\"thirdparty\"")
+ .doesNotContain("Java version", System.getProperty("java.version"))
+ .doesNotContain("Spring Boot version", SpringBootVersion.getVersion());
+ }
+
+ @Test
+ void about_shows_the_build_version_when_build_info_is_present() throws Exception {
+ BuildProperties build = buildProperties.getIfAvailable();
+ String html = mockMvc.perform(get("/about"))
+ .andExpect(status().isOk())
+ .andReturn().getResponse().getContentAsString();
+
+ // build-info.properties is generated by spring-boot-maven-plugin:build-info in a Maven build
+ assertThat(build).as("BuildProperties (spring-boot-maven-plugin:build-info)").isNotNull();
+ assertThat(html).contains(build.getVersion(), AboutController.releaseStatus(build.getVersion()), "Build time");
+ }
+
+ @Test
+ void footer_links_to_the_about_page() throws Exception {
+ String html = mockMvc.perform(get("/"))
+ .andExpect(status().isOk())
+ .andReturn().getResponse().getContentAsString();
+
+ assertThat(html).contains("/about\">About");
+ }
+
+ @Test
+ void release_status_follows_the_version() {
+ assertThat(AboutController.releaseStatus("4.0.0-SNAPSHOT")).startsWith("Pre-release");
+ assertThat(AboutController.releaseStatus(null)).startsWith("Pre-release");
+ assertThat(AboutController.releaseStatus("4.0.0")).isEqualTo("Release");
+ }
+}
diff --git a/openespi-thirdparty/src/test/java/org/greenbuttonalliance/espi/thirdparty/web/GbaLayoutPagesTest.java b/openespi-thirdparty/src/test/java/org/greenbuttonalliance/espi/thirdparty/web/GbaLayoutPagesTest.java
index 372847b5..17e03eea 100644
--- a/openespi-thirdparty/src/test/java/org/greenbuttonalliance/espi/thirdparty/web/GbaLayoutPagesTest.java
+++ b/openespi-thirdparty/src/test/java/org/greenbuttonalliance/espi/thirdparty/web/GbaLayoutPagesTest.java
@@ -42,19 +42,27 @@ class GbaLayoutPagesTest {
@Autowired
private MockMvc mockMvc;
+ /** The production context path; rendering under it is what makes the #235 check meaningful. */
+ private static final String CONTEXT_PATH = "/ThirdParty";
+
@ParameterizedTest
- @ValueSource(strings = {"/", "/login", "/TermsOfService", "/UsagePolicy"})
+ @ValueSource(strings = {"/", "/login", "/about", "/TermsOfService", "/UsagePolicy"})
void public_pages_render_anonymously_with_the_gba_layout(String path) throws Exception {
- String html = mockMvc.perform(get(path))
+ // Render under the real context path. With MockMvc's default empty context path, a correct
+ // th:href="@{/about}" and a broken href="/about" produce the same HTML, so #235 could not
+ // be detected.
+ String html = mockMvc.perform(get(CONTEXT_PATH + path).contextPath(CONTEXT_PATH))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();
assertThat(html)
- .contains("/css/gba-theme.css", "/images/gba-logo.png", "/images/gba-favicon.png")
+ .contains(CONTEXT_PATH + "/css/gba-theme.css", CONTEXT_PATH + "/images/gba-logo.png",
+ CONTEXT_PATH + "/images/gba-favicon.png")
.contains("gba-navbar", "gba-footer", "data-gba-app=\"thirdparty\"")
.contains("integrity=\"sha384-")
- // no root-relative internal links (#235): the layout uses th:href="@{...}"
- .doesNotContain("href=\"/about\"");
+ // #235: every internal link must carry the context path. Allowed: /ThirdParty...,
+ // protocol-relative //host and absolute https:// links.
+ .doesNotContainPattern("(href|src)=\"/(?!ThirdParty[/\"]|/)");
}
@ParameterizedTest