Apply BaseUrl consistently

This commit updates BaseUrlBuilderFactory to reuse features of the
Spring Framework to handle the base URL rather than duplicating it.

As a result, BaseUrl no longer resolves anything but rather provide
a standard component that is able to resolve the URI if needed.

Closes gh-47674
This commit is contained in:
Stéphane Nicoll
2025-10-17 16:35:06 +02:00
parent 998a2fa7b4
commit 46a3c95a5c
6 changed files with 116 additions and 64 deletions
@@ -32,29 +32,34 @@ import org.springframework.web.util.UriComponentsBuilder;
* {@link UriBuilderFactory} to support {@link BaseUrl}.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @since 4.0.0
*/
public class BaseUrlUriBuilderFactory implements UriBuilderFactory {
private final UriBuilderFactory delegate;
public final class BaseUrlUriBuilderFactory implements UriBuilderFactory {
private final BaseUrl baseUrl;
/**
* Create a new {@link BaseUrlUriBuilderFactory} instance.
* @param delegate the delegate {@link UriBuilderFactory}
* @param baseUrl the base URL to use
*/
public BaseUrlUriBuilderFactory(UriBuilderFactory delegate, BaseUrl baseUrl) {
Assert.notNull(delegate, "'delegate' must not be null");
BaseUrlUriBuilderFactory(BaseUrl baseUrl) {
Assert.notNull(baseUrl, "'baseUrl' must not be null");
this.delegate = delegate;
this.baseUrl = baseUrl;
}
/**
* Get a {@link UriBuilderFactory} instance applying the given {@code baseUrl}.
* @param baseUrl the base URL to apply or {@code null}
* @return a factory for the given base URL
*/
public static UriBuilderFactory get(@Nullable BaseUrl baseUrl) {
return (baseUrl != null) ? new BaseUrlUriBuilderFactory(baseUrl) : new DefaultUriBuilderFactory();
}
@Override
public UriBuilder uriString(String uriTemplate) {
return UriComponentsBuilder.fromUriString(apply(uriTemplate));
return createDelegate().uriString(uriTemplate);
}
@Override
@@ -64,21 +69,16 @@ public class BaseUrlUriBuilderFactory implements UriBuilderFactory {
@Override
public URI expand(String uriTemplate, Map<String, ?> uriVariables) {
return this.delegate.expand(apply(uriTemplate), uriVariables);
return createDelegate().expand(uriTemplate, uriVariables);
}
@Override
public URI expand(String uriTemplate, @Nullable Object... uriVariables) {
return this.delegate.expand(apply(uriTemplate), uriVariables);
return createDelegate().expand(uriTemplate, uriVariables);
}
String apply(String uriTemplate) {
return (uriTemplate.startsWith("/")) ? this.baseUrl.resolve(uriTemplate) : uriTemplate;
}
public static UriBuilderFactory get(@Nullable BaseUrl baseUrl) {
DefaultUriBuilderFactory delegate = new DefaultUriBuilderFactory();
return (baseUrl != null) ? new BaseUrlUriBuilderFactory(delegate, baseUrl) : delegate;
private UriBuilderFactory createDelegate() {
return this.baseUrl.getUriBuilderFactory();
}
}
@@ -18,10 +18,10 @@ package org.springframework.boot.test.http.server;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.UriBuilderFactory;
/**
* A base URL that can be used to connect to the running server.
@@ -44,29 +44,10 @@ public interface BaseUrl {
boolean isHttps();
/**
* Resolve the URL to a string. This method is called as late as possible to ensure
* that an local port information is available.
* @param path the path to append
* @return the resolved base URL
* Get a {@link UriBuilderFactory} that applies the base URL.
* @return a {@link UriBuilderFactory}
*/
default String resolve(@Nullable String path) {
String resolved = resolve();
if (StringUtils.hasLength(path)) {
if (resolved.endsWith("/") && path.startsWith("/")) {
path = path.substring(1);
}
resolved += (resolved.endsWith("/") || path.startsWith("/")) ? "" : "/";
resolved += path;
}
return resolved;
}
/**
* Resolve the URL to a string. This method is called as late as possible to ensure
* that an local port information is available.
* @return the resolved base URL
*/
String resolve();
UriBuilderFactory getUriBuilderFactory();
/**
* Return a new instance that applies the given {@code path}.
@@ -113,7 +94,11 @@ public interface BaseUrl {
}
@Override
public String resolve() {
public UriBuilderFactory getUriBuilderFactory() {
return new DefaultUriBuilderFactory(resolve());
}
String resolve() {
return this.resolver.get();
}
@@ -43,8 +43,8 @@ public class BaseUrlWebClient extends WebClient {
@Override
public <P extends Page> P getPage(String url) throws IOException, FailingHttpStatusCodeException {
if (this.baseUrl != null && url.startsWith("/")) {
url = this.baseUrl.resolve(url);
if (this.baseUrl != null) {
url = this.baseUrl.getUriBuilderFactory().uriString(url).toUriString();
}
return super.getPage(url);
}
@@ -56,8 +56,8 @@ public class BaseUrlWebConnectionHtmlUnitDriver extends WebConnectionHtmlUnitDri
@Override
public void get(String url) {
if (this.baseUrl != null && url.startsWith("/")) {
url = this.baseUrl.resolve(url);
if (this.baseUrl != null) {
url = this.baseUrl.getUriBuilderFactory().uriString(url).toUriString();
}
super.get(url);
}
@@ -0,0 +1,68 @@
/*
* 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.http.client;
import java.net.URI;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.http.server.BaseUrl;
import org.springframework.web.util.UriBuilderFactory;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link BaseUrlUriBuilderFactory}.
*
* @author Stephane Nicoll
*/
class BaseUrlUriBuilderFactoryTests {
@Test
void uriWithRootSlashAddsBaseUrl() {
UriBuilderFactory factory = BaseUrlUriBuilderFactory.get(BaseUrl.of("https://example.com"));
assertThat(factory.uriString("/").build()).isEqualTo(URI.create("https://example.com/"));
}
@Test
void uriWithEmptyAddsBaseUrl() {
UriBuilderFactory factory = BaseUrlUriBuilderFactory.get(BaseUrl.of("https://example.com"));
assertThat(factory.uriString("").build()).isEqualTo(URI.create("https://example.com"));
}
@Test
void uriWithMapVariablesAddsBaseUrl() {
UriBuilderFactory factory = BaseUrlUriBuilderFactory.get(BaseUrl.of("https://example.com"));
assertThat(factory.expand("/test/{name}", Map.of("name", "value")))
.isEqualTo(URI.create("https://example.com/test/value"));
}
@Test
void uriWithVariablesAddsBaseUrl() {
UriBuilderFactory factory = BaseUrlUriBuilderFactory.get(BaseUrl.of("https://example.com"));
assertThat(factory.expand("/test/{name}", "value")).isEqualTo(URI.create("https://example.com/test/value"));
}
@Test
void uriWithHostDoesNotExpandBaseUrl() {
UriBuilderFactory factory = BaseUrlUriBuilderFactory.get(BaseUrl.of("https://example.com"));
assertThat(factory.uriString("https://sub.example.com").build())
.isEqualTo(URI.create("https://sub.example.com"));
}
}
@@ -32,33 +32,29 @@ class BaseUrlTests {
@Test
void resolveWithString() {
assertThat(BaseUrl.of("http://localhost").resolve(null)).isEqualTo("http://localhost");
assertThat(BaseUrl.of("http://localhost").resolve("")).isEqualTo("http://localhost");
assertThat(BaseUrl.of("http://localhost").resolve("path")).isEqualTo("http://localhost/path");
assertThat(BaseUrl.of("http://localhost").resolve("/path")).isEqualTo("http://localhost/path");
assertThat(BaseUrl.of("http://localhost/").resolve("path")).isEqualTo("http://localhost/path");
assertThat(BaseUrl.of("http://localhost/").resolve("/path")).isEqualTo("http://localhost/path");
assertThat(resolve(BaseUrl.of("http://localhost"), "")).isEqualTo("http://localhost");
assertThat(resolve(BaseUrl.of("http://localhost"), "/path")).isEqualTo("http://localhost/path");
assertThat(resolve(BaseUrl.of("http://localhost/"), "/path")).isEqualTo("http://localhost/path");
}
@Test
void ofWhenHttp() {
BaseUrl baseUrl = BaseUrl.of("http://localhost:8080/context");
assertThat(baseUrl.isHttps()).isFalse();
assertThat(baseUrl.resolve()).isEqualTo("http://localhost:8080/context");
assertThat(resolve(baseUrl, "")).isEqualTo("http://localhost:8080/context");
}
@Test
void ofWhenHttps() {
BaseUrl baseUrl = BaseUrl.of("https://localhost:8080/context");
assertThat(baseUrl.isHttps()).isTrue();
assertThat(baseUrl.resolve()).isEqualTo("https://localhost:8080/context");
assertThat(resolve(baseUrl, "")).isEqualTo("https://localhost:8080/context");
}
@Test
void ofWhenUppercaseHttps() {
BaseUrl baseUrl = BaseUrl.of("HTTPS://localhost:8080/context");
assertThat(baseUrl.isHttps()).isTrue();
assertThat(baseUrl.resolve()).isEqualTo("HTTPS://localhost:8080/context");
}
@Test
@@ -73,8 +69,8 @@ class BaseUrlTests {
BaseUrl baseUrl = BaseUrl.of(true, () -> String.valueOf(atomicInteger.incrementAndGet()));
assertThat(atomicInteger.get()).isZero();
assertThat(baseUrl.isHttps()).isTrue();
assertThat(baseUrl.resolve()).isEqualTo("1");
assertThat(baseUrl.resolve()).isEqualTo("2");
assertThat(resolve(baseUrl, "")).isEqualTo("1");
assertThat(resolve(baseUrl, "")).isEqualTo("2");
}
@Test
@@ -87,19 +83,22 @@ class BaseUrlTests {
@Test
void withPath() {
BaseUrl baseUrl = BaseUrl.of("http://localhost");
assertThat(baseUrl.withPath("/context").resolve("")).isEqualTo("http://localhost/context");
assertThat(baseUrl.withPath("/context").withPath("/test").resolve("path"))
assertThat(resolve(baseUrl.withPath("/context"), "")).isEqualTo("http://localhost/context");
assertThat(resolve(baseUrl.withPath("/context").withPath("/test"), "/path"))
.isEqualTo("http://localhost/context/test/path");
}
@Test
void withPathInvokesParentResolver() {
AtomicInteger atomicInteger = new AtomicInteger();
BaseUrl baseUrl = BaseUrl.of(true,
() -> "https://example.com/" + atomicInteger.incrementAndGet());
assertThat(baseUrl.withPath("/context").resolve("")).isEqualTo("https://example.com/1/context");
assertThat(baseUrl.withPath("/context").withPath("/test").resolve("path"))
.isEqualTo("https://example.com/2/context/test/path");
BaseUrl baseUrl = BaseUrl.of(true, () -> "https://example.com/" + atomicInteger.incrementAndGet());
assertThat(resolve(baseUrl.withPath("/context"), "")).isEqualTo("https://example.com/1/context");
assertThat(resolve(baseUrl.withPath("/context").withPath("/test"), "/path"))
.isEqualTo("https://example.com/2/context/test/path");
}
private String resolve(BaseUrl baseUrl, String path) {
return baseUrl.getUriBuilderFactory().uriString(path).toUriString();
}
}