diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/http/server/BaseUrl.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/http/server/BaseUrl.java index 275a41e046c..97b9d9832c6 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/http/server/BaseUrl.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/http/server/BaseUrl.java @@ -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 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 resolver; + + private DefaultBaseUrl(boolean https, Supplier 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 updatedResolver = () -> this.resolver.get() + path; + return new DefaultBaseUrl(this.https, updatedResolver); + } - }; } } diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/http/server/BaseUrlTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/http/server/BaseUrlTests.java index a83eff370ed..25e86ad9b67 100644 --- a/core/spring-boot-test/src/test/java/org/springframework/boot/test/http/server/BaseUrlTests.java +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/http/server/BaseUrlTests.java @@ -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"); + } + } diff --git a/module/spring-boot-graphql-test/src/main/java/org/springframework/boot/graphql/test/autoconfigure/tester/HttpGraphQlTesterAutoConfiguration.java b/module/spring-boot-graphql-test/src/main/java/org/springframework/boot/graphql/test/autoconfigure/tester/HttpGraphQlTesterAutoConfiguration.java index 544a6e0810b..f5095506679 100644 --- a/module/spring-boot-graphql-test/src/main/java/org/springframework/boot/graphql/test/autoconfigure/tester/HttpGraphQlTesterAutoConfiguration.java +++ b/module/spring-boot-graphql-test/src/main/java/org/springframework/boot/graphql/test/autoconfigure/tester/HttpGraphQlTesterAutoConfiguration.java @@ -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(); } } diff --git a/module/spring-boot-graphql-test/src/test/java/org/springframework/boot/graphql/test/autoconfigure/tester/HttpGraphQlTesterAutoConfigurationTests.java b/module/spring-boot-graphql-test/src/test/java/org/springframework/boot/graphql/test/autoconfigure/tester/HttpGraphQlTesterAutoConfigurationTests.java new file mode 100644 index 00000000000..7504e5de0ba --- /dev/null +++ b/module/spring-boot-graphql-test/src/test/java/org/springframework/boot/graphql/test/autoconfigure/tester/HttpGraphQlTesterAutoConfigurationTests.java @@ -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"); + } + + } + +} diff --git a/smoke-test/spring-boot-smoke-test-graphql/src/test/java/smoketest/graphql/SampleGraphQlApplicationTests.java b/smoke-test/spring-boot-smoke-test-graphql/src/test/java/smoketest/graphql/SampleGraphQlApplicationTests.java new file mode 100644 index 00000000000..b69696eba3e --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-graphql/src/test/java/smoketest/graphql/SampleGraphQlApplicationTests.java @@ -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(); + } + +}