From ef377656252c1fbacdc810a9f5a0972bef9e862e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 1 Oct 2025 17:07:17 -0700 Subject: [PATCH] Add `BaseUrl` backed HTTP Unit support classes Add new HTML Unit support classes that use `BaseUrlProviders` to find the `BaseUrl`. See gh-46356 --- core/spring-boot-test/build.gradle | 4 + .../test/web/htmlunit/BaseUrlWebClient.java | 52 +++++++++ .../BaseUrlWebConnectionHtmlUnitDriver.java | 65 +++++++++++ .../boot/test/web/htmlunit/package-info.java | 23 ++++ .../web/htmlunit/BaseUrlWebClientTests.java | 84 ++++++++++++++ ...seUrlWebConnectionHtmlUnitDriverTests.java | 108 ++++++++++++++++++ module/spring-boot-webmvc-test/build.gradle | 1 - .../MockMvcWebClientAutoConfiguration.java | 11 +- .../MockMvcWebDriverAutoConfiguration.java | 11 +- 9 files changed, 350 insertions(+), 9 deletions(-) create mode 100644 core/spring-boot-test/src/main/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebClient.java create mode 100644 core/spring-boot-test/src/main/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebConnectionHtmlUnitDriver.java create mode 100644 core/spring-boot-test/src/main/java/org/springframework/boot/test/web/htmlunit/package-info.java create mode 100644 core/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebClientTests.java create mode 100644 core/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebConnectionHtmlUnitDriverTests.java diff --git a/core/spring-boot-test/build.gradle b/core/spring-boot-test/build.gradle index b353c3518f3..3ef5e388292 100644 --- a/core/spring-boot-test/build.gradle +++ b/core/spring-boot-test/build.gradle @@ -38,6 +38,10 @@ dependencies { optional("org.hamcrest:hamcrest-library") optional("org.junit.jupiter:junit-jupiter-api") optional("org.mockito:mockito-core") + optional("org.seleniumhq.selenium:htmlunit3-driver") { + exclude(group: "com.sun.activation", module: "jakarta.activation") + } + optional("org.seleniumhq.selenium:selenium-api") optional("org.skyscreamer:jsonassert") optional("org.springframework:spring-web") diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebClient.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebClient.java new file mode 100644 index 00000000000..d7839a1c6b7 --- /dev/null +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebClient.java @@ -0,0 +1,52 @@ +/* + * 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 org.springframework.boot.test.web.htmlunit; + +import java.io.IOException; + +import org.htmlunit.FailingHttpStatusCodeException; +import org.htmlunit.Page; +import org.htmlunit.WebClient; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.test.http.server.BaseUrl; +import org.springframework.boot.test.http.server.BaseUrlProvider; + +/** + * HTML Unit {@link WebClient} that will automatically prefix relative URLs with a + * {@link BaseUrlProvider provided} {@link BaseUrl}. + * + * @author Phillip Webb + * @since 4.0.0 + */ +public class BaseUrlWebClient extends WebClient { + + private @Nullable BaseUrl baseUrl; + + public BaseUrlWebClient(@Nullable BaseUrl baseUrl) { + this.baseUrl = baseUrl; + } + + @Override + public

P getPage(String url) throws IOException, FailingHttpStatusCodeException { + if (this.baseUrl != null && url.startsWith("/")) { + url = this.baseUrl.resolve(url); + } + return super.getPage(url); + } + +} diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebConnectionHtmlUnitDriver.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebConnectionHtmlUnitDriver.java new file mode 100644 index 00000000000..32aaefd218e --- /dev/null +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebConnectionHtmlUnitDriver.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 org.springframework.boot.test.web.htmlunit; + +import org.htmlunit.BrowserVersion; +import org.jspecify.annotations.Nullable; +import org.openqa.selenium.Capabilities; + +import org.springframework.boot.test.http.server.BaseUrl; +import org.springframework.boot.test.http.server.BaseUrlProvider; +import org.springframework.test.web.servlet.htmlunit.webdriver.WebConnectionHtmlUnitDriver; + +/** + * HTML Unit {@link WebConnectionHtmlUnitDriver} that will automatically prefix relative + * URLs with a {@link BaseUrlProvider provided} {@link BaseUrl}. + * + * @author Phillip Webb + * @since 4.0.0 + */ +public class BaseUrlWebConnectionHtmlUnitDriver extends WebConnectionHtmlUnitDriver { + + private @Nullable BaseUrl baseUrl; + + public BaseUrlWebConnectionHtmlUnitDriver(@Nullable BaseUrl baseUrl) { + this.baseUrl = baseUrl; + } + + public BaseUrlWebConnectionHtmlUnitDriver(@Nullable BaseUrl baseUrl, boolean enableJavascript) { + super(enableJavascript); + this.baseUrl = baseUrl; + } + + public BaseUrlWebConnectionHtmlUnitDriver(@Nullable BaseUrl baseUrl, BrowserVersion browserVersion) { + super(browserVersion); + this.baseUrl = baseUrl; + } + + public BaseUrlWebConnectionHtmlUnitDriver(@Nullable BaseUrl baseUrl, Capabilities capabilities) { + super(capabilities); + this.baseUrl = baseUrl; + } + + @Override + public void get(String url) { + if (this.baseUrl != null && url.startsWith("/")) { + url = this.baseUrl.resolve(url); + } + super.get(url); + } + +} diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/web/htmlunit/package-info.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/web/htmlunit/package-info.java new file mode 100644 index 00000000000..53ba5769587 --- /dev/null +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/web/htmlunit/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * HtmlUnit support classes. + */ +@NullMarked +package org.springframework.boot.test.web.htmlunit; + +import org.jspecify.annotations.NullMarked; diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebClientTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebClientTests.java new file mode 100644 index 00000000000..375d4bb995d --- /dev/null +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebClientTests.java @@ -0,0 +1,84 @@ +/* + * 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 org.springframework.boot.test.web.htmlunit; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +import org.htmlunit.StringWebResponse; +import org.htmlunit.WebClient; +import org.htmlunit.WebConnection; +import org.htmlunit.WebResponse; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.http.server.BaseUrl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BaseUrlWebClient}. + * + * @author Phillip Webb + */ +@SuppressWarnings("resource") +class BaseUrlWebClientTests { + + @Test + void createWhenBaseUrlIsNull() throws Exception { + BaseUrlWebClient client = new BaseUrlWebClient(null); + WebConnection connection = mockConnection(); + client.setWebConnection(connection); + assertThatExceptionOfType(MalformedURLException.class).isThrownBy(() -> client.getPage("/test")); + } + + @Test + void getPageWhenUrlIsRelativeUsesBaseUrl() throws Exception { + WebClient client = new BaseUrlWebClient(BaseUrl.of("https://example.com:8080")); + WebConnection connection = mockConnection(); + client.setWebConnection(connection); + client.getPage("/test"); + thenConnectionRequests(connection, new URL("https://example.com:8080/test")); + } + + @Test + void getPageWhenUrlIsNotRelativeUsesUrl() throws Exception { + WebClient client = new BaseUrlWebClient(BaseUrl.of("https://example.com:8080")); + WebConnection connection = mockConnection(); + client.setWebConnection(connection); + client.getPage("https://example.com:9000/test"); + thenConnectionRequests(connection, new URL("https://example.com:9000/test")); + } + + private void thenConnectionRequests(WebConnection connection, URL url) throws IOException { + then(connection).should().getResponse(assertArg((request) -> assertThat(request.getUrl()).isEqualTo(url))); + } + + private WebConnection mockConnection() throws IOException { + WebConnection connection = mock(WebConnection.class); + WebResponse response = new StringWebResponse("test", new URL("http://localhost")); + given(connection.getResponse(any())).willReturn(response); + return connection; + } + +} diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebConnectionHtmlUnitDriverTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebConnectionHtmlUnitDriverTests.java new file mode 100644 index 00000000000..91b2a32e01b --- /dev/null +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/BaseUrlWebConnectionHtmlUnitDriverTests.java @@ -0,0 +1,108 @@ +/* + * 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 org.springframework.boot.test.web.htmlunit; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.htmlunit.TopLevelWindow; +import org.htmlunit.WebClient; +import org.htmlunit.WebClientOptions; +import org.htmlunit.WebConsole; +import org.htmlunit.WebRequest; +import org.htmlunit.WebWindow; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.openqa.selenium.WebDriverException; + +import org.springframework.boot.test.http.server.BaseUrl; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BaseUrlWebConnectionHtmlUnitDriver}. + * + * @author Phillip Webb + */ +class BaseUrlWebConnectionHtmlUnitDriverTests { + + private final WebClient webClient; + + BaseUrlWebConnectionHtmlUnitDriverTests() { + this.webClient = mock(); + given(this.webClient.getOptions()).willReturn(new WebClientOptions()); + given(this.webClient.getWebConsole()).willReturn(new WebConsole()); + WebWindow currentWindow = mock(WebWindow.class); + given(currentWindow.isClosed()).willReturn(false); + given(this.webClient.getCurrentWindow()).willReturn(currentWindow); + } + + @Test + void createWhenBaseUrlIsNull() { + BaseUrlWebConnectionHtmlUnitDriver driver = new TestBaseUrlWebConnectionHtmlUnitDriver(null); + assertThatExceptionOfType(WebDriverException.class).isThrownBy(() -> driver.get("/test")) + .withCauseInstanceOf(MalformedURLException.class); + } + + @Test + void getWhenUrlIsRelativeUsesBaseUrl() throws Exception { + BaseUrl baseUrl = BaseUrl.of("https://example.com"); + BaseUrlWebConnectionHtmlUnitDriver driver = new TestBaseUrlWebConnectionHtmlUnitDriver(baseUrl); + driver.get("/test"); + then(this.webClient).should() + .getPage(any(TopLevelWindow.class), requestToUrl(new URL("https://example.com/test"))); + } + + private WebRequest requestToUrl(URL url) { + return argThat(new WebRequestUrlArgumentMatcher(url)); + } + + public class TestBaseUrlWebConnectionHtmlUnitDriver extends BaseUrlWebConnectionHtmlUnitDriver { + + TestBaseUrlWebConnectionHtmlUnitDriver(@Nullable BaseUrl baseUrl) { + super(baseUrl); + } + + @Override + public WebClient getWebClient() { + return BaseUrlWebConnectionHtmlUnitDriverTests.this.webClient; + } + + } + + private static final class WebRequestUrlArgumentMatcher implements ArgumentMatcher { + + private final URL expectedUrl; + + private WebRequestUrlArgumentMatcher(URL expectedUrl) { + this.expectedUrl = expectedUrl; + } + + @Override + public boolean matches(WebRequest argument) { + return argument.getUrl().equals(this.expectedUrl); + } + + } + +} diff --git a/module/spring-boot-webmvc-test/build.gradle b/module/spring-boot-webmvc-test/build.gradle index 50454445034..b9d2dd7df86 100644 --- a/module/spring-boot-webmvc-test/build.gradle +++ b/module/spring-boot-webmvc-test/build.gradle @@ -32,7 +32,6 @@ dependencies { implementation(project(":module:spring-boot-http-converter")) implementation(project(":module:spring-boot-web-server")) - implementation(project(":module:spring-boot-web-server-test")) optional(project(":core:spring-boot-autoconfigure")) optional(project(":core:spring-boot-test-autoconfigure")) diff --git a/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcWebClientAutoConfiguration.java b/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcWebClientAutoConfiguration.java index 43c047ae245..08c38174a96 100644 --- a/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcWebClientAutoConfiguration.java +++ b/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcWebClientAutoConfiguration.java @@ -23,9 +23,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.web.server.test.htmlunit.LocalHostWebClient; +import org.springframework.boot.test.http.server.BaseUrl; +import org.springframework.boot.test.http.server.BaseUrlProviders; +import org.springframework.boot.test.web.htmlunit.BaseUrlWebClient; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.core.env.Environment; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder; @@ -43,8 +45,9 @@ public final class MockMvcWebClientAutoConfiguration { @Bean @ConditionalOnMissingBean({ WebClient.class, MockMvcWebClientBuilder.class }) @ConditionalOnBean(MockMvc.class) - MockMvcWebClientBuilder mockMvcWebClientBuilder(MockMvc mockMvc, Environment environment) { - return MockMvcWebClientBuilder.mockMvcSetup(mockMvc).withDelegate(new LocalHostWebClient(environment)); + MockMvcWebClientBuilder mockMvcWebClientBuilder(MockMvc mockMvc, ApplicationContext applicationContext) { + BaseUrl baseUrl = new BaseUrlProviders(applicationContext).getBaseUrlOrDefault(); + return MockMvcWebClientBuilder.mockMvcSetup(mockMvc).withDelegate(new BaseUrlWebClient(baseUrl)); } @Bean diff --git a/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcWebDriverAutoConfiguration.java b/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcWebDriverAutoConfiguration.java index 37bbb6f1427..ac2497349f5 100644 --- a/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcWebDriverAutoConfiguration.java +++ b/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcWebDriverAutoConfiguration.java @@ -26,9 +26,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.web.server.test.htmlunit.webdriver.LocalHostWebConnectionHtmlUnitDriver; +import org.springframework.boot.test.http.server.BaseUrl; +import org.springframework.boot.test.http.server.BaseUrlProviders; +import org.springframework.boot.test.web.htmlunit.BaseUrlWebConnectionHtmlUnitDriver; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.core.env.Environment; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder; @@ -46,9 +48,10 @@ public final class MockMvcWebDriverAutoConfiguration { @Bean @ConditionalOnMissingBean({ WebDriver.class, MockMvcHtmlUnitDriverBuilder.class }) @ConditionalOnBean(MockMvc.class) - MockMvcHtmlUnitDriverBuilder mockMvcHtmlUnitDriverBuilder(MockMvc mockMvc, Environment environment) { + MockMvcHtmlUnitDriverBuilder mockMvcHtmlUnitDriverBuilder(MockMvc mockMvc, ApplicationContext applicationContext) { + BaseUrl baseUrl = new BaseUrlProviders(applicationContext).getBaseUrlOrDefault(); MockMvcHtmlUnitDriverBuilder builder = MockMvcHtmlUnitDriverBuilder.mockMvcSetup(mockMvc) - .withDelegate(new LocalHostWebConnectionHtmlUnitDriver(environment, BrowserVersion.CHROME)); + .withDelegate(new BaseUrlWebConnectionHtmlUnitDriver(baseUrl, BrowserVersion.CHROME)); return builder; }