From 1f4a8dff9870ff698388c6335ac8532f1a3a029c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 31 Dec 2025 16:57:05 +0100 Subject: [PATCH] Restore RANDOM_PORT handing in tests with a separate management port This commit fixes a regression where RANDOM_PORT was no longer honored if a defined management port is set. Due to the modularization efforts, the code has moved from an EnvironmentPostProcessor to an ApplicationListener. Unfortunately, the listener is registered too late to handle the event it is listening to. While the event type could have been changed, the listener was added on the ApplicationContext which are not honored before the ApplicationContext is in a state to be used. The contract of ContextCustomizerFactory is already giving us everything we need. While the environment is post-processed later than we would like, it is still post-processed before the refresh state so that the additional property is honored. This commit also adds an integration test to cover this scenario. Closes gh-48653 --- ...gBootTestRandomPortContextCustomizer.java} | 34 +++++----- ...estRandomPortContextCustomizerFactory.java | 27 +------- ...TestRandomPortContextCustomizerTests.java} | 34 +++++----- ...redPortSampleActuatorApplicationTests.java | 65 +++++++++++++++++++ 4 files changed, 101 insertions(+), 59 deletions(-) rename module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/{SpringBootTestRandomPortApplicationListener.java => SpringBootTestRandomPortContextCustomizer.java} (86%) rename module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/context/{SpringBootTestRandomPortApplicationListenerTests.java => SpringBootTestRandomPortContextCustomizerTests.java} (88%) create mode 100644 smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementRandomPortWithConfiguredPortSampleActuatorApplicationTests.java diff --git a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortApplicationListener.java b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortContextCustomizer.java similarity index 86% rename from module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortApplicationListener.java rename to module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortContextCustomizer.java index 00bf9b01301..e43ca14a349 100644 --- a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortApplicationListener.java +++ b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortContextCustomizer.java @@ -20,10 +20,8 @@ import java.util.Objects; import org.jspecify.annotations.Nullable; -import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; -import org.springframework.context.ApplicationEvent; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.event.SmartApplicationListener; -import org.springframework.core.Ordered; import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.env.ConfigurableEnvironment; @@ -33,6 +31,8 @@ import org.springframework.core.env.PropertyResolver; import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySources; import org.springframework.lang.Contract; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.util.ClassUtils; @@ -45,25 +45,13 @@ import org.springframework.util.ClassUtils; * @author Andy Wilkinson * @author Phillip Webb */ -class SpringBootTestRandomPortApplicationListener implements SmartApplicationListener, Ordered { +class SpringBootTestRandomPortContextCustomizer implements ContextCustomizer { private static final String TEST_SOURCE_NAME = TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME; @Override - public int getOrder() { - return Ordered.LOWEST_PRECEDENCE; - } - - @Override - public boolean supportsEventType(Class eventType) { - return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType); - } - - @Override - public void onApplicationEvent(ApplicationEvent event) { - if (event instanceof ApplicationEnvironmentPreparedEvent environmentPreparedEvent) { - postProcessEnvironment(environmentPreparedEvent.getEnvironment()); - } + public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { + postProcessEnvironment(context.getEnvironment()); } void postProcessEnvironment(ConfigurableEnvironment environment) { @@ -92,6 +80,16 @@ class SpringBootTestRandomPortApplicationListener implements SmartApplicationLis return (!managementPort.equals(serverPort)) ? "0" : ""; } + @Override + public boolean equals(Object obj) { + return (obj != null) && (obj.getClass() == getClass()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + private enum Port { SERVER("server.port"), MANAGEMENT("management.server.port"); diff --git a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortContextCustomizerFactory.java b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortContextCustomizerFactory.java index 0c336e5e436..66f0d325c2e 100644 --- a/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortContextCustomizerFactory.java +++ b/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortContextCustomizerFactory.java @@ -20,15 +20,13 @@ import java.util.List; import org.jspecify.annotations.Nullable; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; -import org.springframework.test.context.MergedContextConfiguration; /** - * {@link ContextCustomizerFactory} apply - * {@link SpringBootTestRandomPortApplicationListener} to tests. + * {@link ContextCustomizerFactory} implementation to apply + * {@link SpringBootTestRandomPortContextCustomizer} to tests. * * @author Phillip Webb */ @@ -37,26 +35,7 @@ class SpringBootTestRandomPortContextCustomizerFactory implements ContextCustomi @Override public @Nullable ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { - return new Customizer(); - } - - static class Customizer implements ContextCustomizer { - - @Override - public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { - context.addApplicationListener(new SpringBootTestRandomPortApplicationListener()); - } - - @Override - public boolean equals(Object obj) { - return (obj != null) && (obj.getClass() == getClass()); - } - - @Override - public int hashCode() { - return getClass().hashCode(); - } - + return new SpringBootTestRandomPortContextCustomizer(); } } diff --git a/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortApplicationListenerTests.java b/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortContextCustomizerTests.java similarity index 88% rename from module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortApplicationListenerTests.java rename to module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortContextCustomizerTests.java index 1d8439f71f6..9e67b726466 100644 --- a/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortApplicationListenerTests.java +++ b/module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/context/SpringBootTestRandomPortContextCustomizerTests.java @@ -34,14 +34,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Tests for {@link SpringBootTestRandomPortApplicationListener}. + * Tests for {@link SpringBootTestRandomPortContextCustomizer}. * * @author Madhura Bhave * @author Andy Wilkinson */ -class SpringBootTestRandomPortApplicationListenerTests { +class SpringBootTestRandomPortContextCustomizerTests { - private final SpringBootTestRandomPortApplicationListener listener = new SpringBootTestRandomPortApplicationListener(); + private final SpringBootTestRandomPortContextCustomizer customizer = new SpringBootTestRandomPortContextCustomizer(); private MockEnvironment environment; @@ -56,7 +56,7 @@ class SpringBootTestRandomPortApplicationListenerTests { @Test void postProcessWhenServerAndManagementPortIsZeroInTestPropertySource() { addTestPropertySource("0", "0"); - this.listener.postProcessEnvironment(this.environment); + this.customizer.postProcessEnvironment(this.environment); assertThat(this.environment.getProperty("server.port")).isEqualTo("0"); assertThat(this.environment.getProperty("management.server.port")).isEqualTo("0"); } @@ -67,7 +67,7 @@ class SpringBootTestRandomPortApplicationListenerTests { Map source = new HashMap<>(); source.put("management.server.port", "0"); this.propertySources.addLast(new MapPropertySource("other", source)); - this.listener.postProcessEnvironment(this.environment); + this.customizer.postProcessEnvironment(this.environment); assertThat(this.environment.getProperty("server.port")).isEqualTo("0"); assertThat(this.environment.getProperty("management.server.port")).isEqualTo("0"); } @@ -77,7 +77,7 @@ class SpringBootTestRandomPortApplicationListenerTests { addTestPropertySource("8080", "8081"); this.environment.setProperty("server.port", "8080"); this.environment.setProperty("management.server.port", "8081"); - this.listener.postProcessEnvironment(this.environment); + this.customizer.postProcessEnvironment(this.environment); assertThat(this.environment.getProperty("server.port")).isEqualTo("8080"); assertThat(this.environment.getProperty("management.server.port")).isEqualTo("8081"); } @@ -85,7 +85,7 @@ class SpringBootTestRandomPortApplicationListenerTests { @Test void postProcessWhenTestServerPortIsZeroAndTestManagementPortIsNotNull() { addTestPropertySource("0", "8080"); - this.listener.postProcessEnvironment(this.environment); + this.customizer.postProcessEnvironment(this.environment); assertThat(this.environment.getProperty("server.port")).isEqualTo("0"); assertThat(this.environment.getProperty("management.server.port")).isEqualTo("8080"); } @@ -93,7 +93,7 @@ class SpringBootTestRandomPortApplicationListenerTests { @Test void postProcessWhenTestServerPortIsZeroAndManagementPortIsNull() { addTestPropertySource("0", null); - this.listener.postProcessEnvironment(this.environment); + this.customizer.postProcessEnvironment(this.environment); assertThat(this.environment.getProperty("server.port")).isEqualTo("0"); assertThat(this.environment.getProperty("management.server.port")).isNull(); } @@ -106,7 +106,7 @@ class SpringBootTestRandomPortApplicationListenerTests { other.put("management.server.port", "8081"); MapPropertySource otherSource = new MapPropertySource("other", other); this.propertySources.addLast(otherSource); - this.listener.postProcessEnvironment(this.environment); + this.customizer.postProcessEnvironment(this.environment); assertThat(this.environment.getProperty("server.port")).isEqualTo("0"); assertThat(this.environment.getProperty("management.server.port")).isEmpty(); } @@ -118,7 +118,7 @@ class SpringBootTestRandomPortApplicationListenerTests { addTestPropertySource("0", null); this.propertySources .addLast(new MapPropertySource("other", Collections.singletonMap("management.server.port", "8080"))); - this.listener.postProcessEnvironment(this.environment); + this.customizer.postProcessEnvironment(this.environment); assertThat(this.environment.getProperty("server.port")).isEqualTo("0"); assertThat(this.environment.getProperty("management.server.port")).isEmpty(); } @@ -128,7 +128,7 @@ class SpringBootTestRandomPortApplicationListenerTests { addTestPropertySource("0", null); this.propertySources .addLast(new MapPropertySource("other", Collections.singletonMap("management.server.port", "8081"))); - this.listener.postProcessEnvironment(this.environment); + this.customizer.postProcessEnvironment(this.environment); assertThat(this.environment.getProperty("server.port")).isEqualTo("0"); assertThat(this.environment.getProperty("management.server.port")).isEqualTo("0"); } @@ -138,7 +138,7 @@ class SpringBootTestRandomPortApplicationListenerTests { addTestPropertySource("0", null); this.propertySources .addLast(new MapPropertySource("other", Collections.singletonMap("management.server.port", "-1"))); - this.listener.postProcessEnvironment(this.environment); + this.customizer.postProcessEnvironment(this.environment); assertThat(this.environment.getProperty("server.port")).isEqualTo("0"); assertThat(this.environment.getProperty("management.server.port")).isEqualTo("-1"); } @@ -148,7 +148,7 @@ class SpringBootTestRandomPortApplicationListenerTests { addTestPropertySource("0", null); this.propertySources .addLast(new MapPropertySource("other", Collections.singletonMap("management.server.port", 8081))); - this.listener.postProcessEnvironment(this.environment); + this.customizer.postProcessEnvironment(this.environment); assertThat(this.environment.getProperty("server.port")).isEqualTo("0"); assertThat(this.environment.getProperty("management.server.port")).isEqualTo("0"); } @@ -162,7 +162,7 @@ class SpringBootTestRandomPortApplicationListenerTests { testPropertySource.getSource().put("port", "9090"); this.propertySources .addLast(new MapPropertySource("other", Collections.singletonMap("management.server.port", "${port}"))); - this.listener.postProcessEnvironment(this.environment); + this.customizer.postProcessEnvironment(this.environment); assertThat(this.environment.getProperty("server.port")).isEqualTo("0"); assertThat(this.environment.getProperty("management.server.port")).isEqualTo("0"); } @@ -173,7 +173,7 @@ class SpringBootTestRandomPortApplicationListenerTests { this.propertySources .addLast(new MapPropertySource("other", Collections.singletonMap("management.server.port", "${port}"))); assertThatExceptionOfType(PlaceholderResolutionException.class) - .isThrownBy(() -> this.listener.postProcessEnvironment(this.environment)) + .isThrownBy(() -> this.customizer.postProcessEnvironment(this.environment)) .withMessage("Could not resolve placeholder 'port' in value \"${port}\""); } @@ -188,7 +188,7 @@ class SpringBootTestRandomPortApplicationListenerTests { source.put("server.port", "${port}"); source.put("management.server.port", "9090"); this.propertySources.addLast(new MapPropertySource("other", source)); - this.listener.postProcessEnvironment(this.environment); + this.customizer.postProcessEnvironment(this.environment); assertThat(this.environment.getProperty("server.port")).isEqualTo("0"); assertThat(this.environment.getProperty("management.server.port")).isEqualTo("0"); } @@ -201,7 +201,7 @@ class SpringBootTestRandomPortApplicationListenerTests { source.put("management.server.port", "9090"); this.propertySources.addLast(new MapPropertySource("other", source)); assertThatExceptionOfType(PlaceholderResolutionException.class) - .isThrownBy(() -> this.listener.postProcessEnvironment(this.environment)) + .isThrownBy(() -> this.customizer.postProcessEnvironment(this.environment)) .withMessage("Could not resolve placeholder 'port' in value \"${port}\""); } diff --git a/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementRandomPortWithConfiguredPortSampleActuatorApplicationTests.java b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementRandomPortWithConfiguredPortSampleActuatorApplicationTests.java new file mode 100644 index 00000000000..8ce53f3fd59 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/ManagementRandomPortWithConfiguredPortSampleActuatorApplicationTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-present the original author or authors. + * + * 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 smoketest.actuator; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for separate management and main service ports when a management port + * is set. + * + * @author Stephane Nicoll + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, args = "--management.server.port=1234") +class ManagementRandomPortWithConfiguredPortSampleActuatorApplicationTests { + + @LocalServerPort + private int serverPort; + + @LocalManagementPort + private int managementPort; + + @Test + void randomPortsShouldBeDifferent() { + assertThat(this.serverPort).isPositive(); + assertThat(this.managementPort).isPositive(); + assertThat(this.serverPort).isNotEqualTo(this.managementPort); + } + + @Test + void randomManagementPortShouldBeApplied() { + assertThat(this.managementPort).isNotEqualTo(1234); + } + + @Test + void testHealth() { + ResponseEntity entity = new TestRestTemplate().withBasicAuth("user", "password") + .getForEntity("http://localhost:" + this.managementPort + "/actuator/health", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("\"status\":\"UP\""); + } + +}