Add `BaseUrl` backed HTTP Unit support classes

Add new HTML Unit support classes that use `BaseUrlProviders`
to find the `BaseUrl`.

See gh-46356
This commit is contained in:
Phillip Webb 2025-10-01 17:07:17 -07:00
parent bba56ffc8b
commit ef37765625
9 changed files with 350 additions and 9 deletions

View File

@ -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")

View File

@ -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 extends Page> P getPage(String url) throws IOException, FailingHttpStatusCodeException {
if (this.baseUrl != null && url.startsWith("/")) {
url = this.baseUrl.resolve(url);
}
return super.getPage(url);
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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<WebRequest> {
private final URL expectedUrl;
private WebRequestUrlArgumentMatcher(URL expectedUrl) {
this.expectedUrl = expectedUrl;
}
@Override
public boolean matches(WebRequest argument) {
return argument.getUrl().equals(this.expectedUrl);
}
}
}

View File

@ -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"))

View File

@ -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

View File

@ -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;
}