Fix configuration of HttpGraphQlTester with a running server

This commit restores the user of HttpGraphQlTester when it is configured
against a running server. The logic that appends the graphQl path to
the HTTP url was lost while refactoring the HTTP clients infrastructure.

To work against the new API, BaseUrl has been updated to provide the
ability to append a path to its URL

Closes gh-47659
This commit is contained in:
Stéphane Nicoll
2025-10-17 16:35:06 +02:00
parent 1da46813b8
commit 998a2fa7b4
5 changed files with 250 additions and 16 deletions
@@ -27,6 +27,7 @@ import org.springframework.util.StringUtils;
* A base URL that can be used to connect to the running server.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @since 4.0.0
*/
public interface BaseUrl {
@@ -67,6 +68,13 @@ public interface BaseUrl {
*/
String resolve();
/**
* Return a new instance that applies the given {@code path}.
* @param path a path to append
* @return a new instance with the path added
*/
BaseUrl withPath(String path);
/**
* Factory method to create a new {@link BaseUrl}.
* @param url the URL to use
@@ -84,20 +92,37 @@ public interface BaseUrl {
* @return a new {@link BaseUrl} instance
*/
static BaseUrl of(boolean https, Supplier<String> resolver) {
Assert.notNull(resolver, "'resolver' must not be null");
return new BaseUrl() {
return new DefaultBaseUrl(https, resolver);
}
@Override
public boolean isHttps() {
return https;
}
final class DefaultBaseUrl implements BaseUrl {
@Override
public String resolve() {
return resolver.get();
}
private final boolean https;
private final Supplier<String> resolver;
private DefaultBaseUrl(boolean https, Supplier<String> resolver) {
Assert.notNull(resolver, "'resolver' must not be null");
this.https = https;
this.resolver = resolver;
}
@Override
public boolean isHttps() {
return this.https;
}
@Override
public String resolve() {
return this.resolver.get();
}
@Override
public BaseUrl withPath(String path) {
Supplier<String> updatedResolver = () -> this.resolver.get() + path;
return new DefaultBaseUrl(this.https, updatedResolver);
}
};
}
}
@@ -84,4 +84,22 @@ class BaseUrlTests {
.withMessage("'resolver' must not be null");
}
@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"))
.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");
}
}
@@ -16,11 +16,18 @@
package org.springframework.boot.graphql.test.autoconfigure.tester;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.graphql.autoconfigure.GraphQlProperties;
import org.springframework.boot.test.http.client.BaseUrlUriBuilderFactory;
import org.springframework.boot.test.http.server.BaseUrl;
import org.springframework.boot.test.http.server.BaseUrlProviders;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
import org.springframework.graphql.test.tester.WebGraphQlTester;
@@ -31,19 +38,32 @@ import org.springframework.web.reactive.function.client.WebClient;
* Auto-configuration for {@link HttpGraphQlTester}.
*
* @author Brian Clozel
* @author Stephane Nicoll
* @since 4.0.0
*/
@AutoConfiguration(afterName = { "org.springframework.boot.webtestclient.WebTestClientAutoConfiguration",
"org.springframework.boot.webmvc.test.autoconfigure.MockMvcAutoConfiguration" })
@AutoConfiguration(afterName = "org.springframework.boot.webtestclient.WebTestClientAutoConfiguration")
@ConditionalOnClass({ WebClient.class, WebTestClient.class, WebGraphQlTester.class })
@EnableConfigurationProperties(GraphQlProperties.class)
public final class HttpGraphQlTesterAutoConfiguration {
@Bean
@ConditionalOnBean(WebTestClient.class)
@ConditionalOnMissingBean
HttpGraphQlTester webTestClientGraphQlTester(WebTestClient webTestClient, GraphQlProperties properties) {
WebTestClient mutatedWebTestClient = webTestClient.mutate().baseUrl(properties.getHttp().getPath()).build();
return HttpGraphQlTester.create(mutatedWebTestClient);
HttpGraphQlTester webTestClientGraphQlTester(ApplicationContext applicationContext, WebTestClient webTestClient,
GraphQlProperties properties) {
String graphQlPath = properties.getHttp().getPath();
BaseUrl baseUrl = new BaseUrlProviders(applicationContext).getBaseUrl();
WebTestClient graphQlWebTestClient = configureGraphQlWebTestClient(webTestClient, baseUrl, graphQlPath);
return HttpGraphQlTester.create(graphQlWebTestClient);
}
private WebTestClient configureGraphQlWebTestClient(WebTestClient webTestClient, @Nullable BaseUrl baseUrl,
String graphQlPath) {
WebTestClient.Builder builder = webTestClient.mutate();
if (baseUrl != null) {
return builder.uriBuilderFactory(BaseUrlUriBuilderFactory.get(baseUrl.withPath(graphQlPath))).build();
}
return builder.baseUrl(graphQlPath).build();
}
}
@@ -0,0 +1,115 @@
/*
* 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.graphql.test.autoconfigure.tester;
import java.net.URI;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.http.server.BaseUrl;
import org.springframework.boot.test.http.server.BaseUrlProvider;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.boot.webtestclient.WebTestClientAutoConfiguration;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.util.UriBuilderFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link HttpGraphQlTesterAutoConfiguration}.
*
* @author Stephane Nicoll
*/
class HttpGraphQlTesterAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HttpGraphQlTesterAutoConfiguration.class));
@Test
void shouldNotContributeTesterIfWebTestClientNotPresent() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(HttpGraphQlTester.class));
}
@Test
void shouldContributeTesterBoutToMockMvc() {
this.contextRunner.withBean(MockMvc.class, () -> mock(MockMvc.class))
.withConfiguration(AutoConfigurations.of(WebTestClientAutoConfiguration.class))
.run((context) -> {
assertThat(context).hasSingleBean(HttpGraphQlTester.class);
assertThat(context.getBean(HttpGraphQlTester.class)).extracting("webTestClient")
.extracting("builder")
.extracting("baseUrl")
.isEqualTo("/graphql");
});
}
@Test
@WithResource(name = "META-INF/spring.factories",
content = """
org.springframework.boot.test.http.server.BaseUrlProvider=\
org.springframework.boot.graphql.test.autoconfigure.tester.HttpGraphQlTesterAutoConfigurationTests$TestBaseUrlProvider
""")
void shouldContributeTesterBoundToHttpServer() {
this.contextRunner.withConfiguration(AutoConfigurations.of(WebTestClientAutoConfiguration.class))
.run((context) -> {
assertThat(context).hasSingleBean(HttpGraphQlTester.class);
assertThat(context.getBean(HttpGraphQlTester.class)).extracting("webTestClient")
.extracting("builder")
.extracting("uriBuilderFactory")
.asInstanceOf(InstanceOfAssertFactories.type(UriBuilderFactory.class))
.satisfies((uriBuilderFactory) -> assertThat(uriBuilderFactory.uriString("/something").build())
.isEqualTo(URI.create("https://localhost:4242/graphql/something")));
});
}
@Test
@WithResource(name = "META-INF/spring.factories",
content = """
org.springframework.boot.test.http.server.BaseUrlProvider=\
org.springframework.boot.graphql.test.autoconfigure.tester.HttpGraphQlTesterAutoConfigurationTests$TestBaseUrlProvider
""")
void shouldContributeTesterBoundToHttpServerUsingCustomGraphQlHttpPath() {
this.contextRunner.withConfiguration(AutoConfigurations.of(WebTestClientAutoConfiguration.class))
.withPropertyValues("spring.graphql.http.path=/api/graphql")
.run((context) -> {
assertThat(context).hasSingleBean(HttpGraphQlTester.class);
assertThat(context.getBean(HttpGraphQlTester.class)).extracting("webTestClient")
.extracting("builder")
.extracting("uriBuilderFactory")
.asInstanceOf(InstanceOfAssertFactories.type(UriBuilderFactory.class))
.satisfies((uriBuilderFactory) -> assertThat(uriBuilderFactory.uriString("/something").build())
.isEqualTo(URI.create("https://localhost:4242/api/graphql/something")));
});
}
@SuppressWarnings("unused")
static class TestBaseUrlProvider implements BaseUrlProvider {
@Override
public @Nullable BaseUrl getBaseUrl() {
return BaseUrl.of("https://localhost:4242");
}
}
}
@@ -0,0 +1,56 @@
/*
* 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.graphql;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.graphql.test.autoconfigure.tester.AutoConfigureHttpGraphQlTester;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
/**
* Tests for {@link SampleGraphQlApplication}.
*
* @author Stephane Nicoll
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureHttpGraphQlTester
class SampleGraphQlApplicationTests {
@Autowired
private HttpGraphQlTester graphQlTester;
@Test
void shouldFindSpringGraphQl() {
this.graphQlTester.document("{ project(slug: \"spring-graphql\") { name } }")
.execute()
.path("project.name")
.entity(String.class)
.isEqualTo("Spring GraphQL");
}
@Test
void shouldNotFindUnknownProject() {
this.graphQlTester.document("{ project(slug: \"spring-unknown\") { name } }")
.execute()
.path("project.name")
.pathDoesNotExist();
}
}