Skip to content

Commit 1fe41cc

Browse files
fix(graphql-java-kickstart#487): Playground integration not working when using WebFlux
1 parent 3afbfee commit 1fe41cc

File tree

11 files changed

+214
-19
lines changed

11 files changed

+214
-19
lines changed

example-graphql-subscription/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ apply plugin: "org.springframework.boot"
33
dependencies {
44
implementation(project(":graphql-spring-boot-starter"))
55
implementation(project(":graphiql-spring-boot-starter"))
6+
implementation(project(":playground-spring-boot-starter"))
67
implementation "com.graphql-java-kickstart:graphql-java-tools:$LIB_GRAPHQL_JAVA_TOOLS_VER"
78

89
implementation "io.reactivex.rxjava2:rxjava"

example-webflux/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dependencies {
2323
implementation(project(":graphql-kickstart-spring-boot-starter-webflux"))
2424
implementation(project(":graphql-kickstart-spring-boot-starter-tools"))
2525
implementation(project(":voyager-spring-boot-starter"))
26+
implementation(project(":playground-spring-boot-starter"))
2627

2728
implementation("org.springframework.boot:spring-boot-starter-webflux:$LIB_SPRING_BOOT_VER")
2829
implementation("org.springframework.boot:spring-boot-starter-actuator:$LIB_SPRING_BOOT_VER")

playground-spring-boot-autoconfigure/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ dependencies{
2020
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
2121

2222
implementation "org.springframework.boot:spring-boot-autoconfigure"
23-
implementation "org.springframework.boot:spring-boot-starter-web"
23+
compileOnly "org.springframework.boot:spring-boot-starter-web"
24+
compileOnly "org.springframework.boot:spring-boot-starter-webflux"
2425
implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
2526
implementation "org.springframework.boot:spring-boot-starter-validation"
2627

2728
testImplementation "org.springframework.boot:spring-boot-starter-web"
29+
testImplementation "org.springframework.boot:spring-boot-starter-webflux"
2830
testImplementation "org.springframework.boot:spring-boot-starter-test"
2931
testImplementation "org.springframework.boot:spring-boot-starter-security"
3032
testImplementation "org.jsoup:jsoup:$LIB_JSOUP_VER"

playground-spring-boot-autoconfigure/src/main/java/graphql/kickstart/playground/boot/PlaygroundAutoConfiguration.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
package graphql.kickstart.playground.boot;
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
4-
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
54
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
65
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
76
import org.springframework.boot.context.properties.EnableConfigurationProperties;
87
import org.springframework.context.annotation.Bean;
98
import org.springframework.context.annotation.Configuration;
10-
import org.springframework.web.servlet.DispatcherServlet;
119

1210
@Configuration
1311
@ConditionalOnWebApplication
14-
@ConditionalOnClass(DispatcherServlet.class)
1512
@EnableConfigurationProperties(PlaygroundPropertiesConfiguration.class)
1613
public class PlaygroundAutoConfiguration {
1714

1815
@Bean
1916
@ConditionalOnProperty(value = "graphql.playground.enabled", havingValue = "true", matchIfMissing = true)
20-
PlaygroundController playgroundController(final PlaygroundPropertiesConfiguration playgroundPropertiesConfiguration,
17+
public PlaygroundController playgroundController(final PlaygroundPropertiesConfiguration playgroundPropertiesConfiguration,
2118
final ObjectMapper objectMapper) {
2219
return new PlaygroundController(playgroundPropertiesConfiguration, objectMapper);
2320
}

playground-spring-boot-autoconfigure/src/main/java/graphql/kickstart/playground/boot/PlaygroundController.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import org.springframework.stereotype.Controller;
66
import org.springframework.ui.Model;
77
import org.springframework.web.bind.annotation.GetMapping;
8+
import org.springframework.web.bind.annotation.RequestAttribute;
89

9-
import javax.servlet.http.HttpServletRequest;
1010
import java.nio.file.Paths;
1111

1212
@Controller
@@ -23,21 +23,22 @@ public class PlaygroundController {
2323
private static final String FAVICON_URL_ATTRIBUTE_NAME = "faviconUrl";
2424
private static final String SCRIPT_URL_ATTRIBUTE_NAME = "scriptUrl";
2525
private static final String LOGO_URL_ATTRIBUTE_NAME = "logoUrl";
26+
private static final String _CSRF = "_csrf";
2627

2728
private final PlaygroundPropertiesConfiguration propertiesConfiguration;
2829

2930
private final ObjectMapper objectMapper;
3031

3132
@GetMapping("${graphql.playground.mapping:/playground}")
32-
public String playground(final Model model, final HttpServletRequest request) {
33+
public String playground(final Model model, final @RequestAttribute(value = _CSRF, required = false) Object csrf) {
3334
if (propertiesConfiguration.getPlayground().getCdn().isEnabled()) {
3435
setCdnUrls(model);
3536
} else {
3637
setLocalAssetUrls(model);
3738
}
3839
model.addAttribute("pageTitle", propertiesConfiguration.getPlayground().getPageTitle());
3940
model.addAttribute("properties", objectMapper.valueToTree(propertiesConfiguration.getPlayground()));
40-
model.addAttribute("_csrf", request.getAttribute("_csrf"));
41+
model.addAttribute(_CSRF, csrf);
4142
return "playground";
4243
}
4344

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package graphql.kickstart.playground.boot;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
5+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
6+
import org.springframework.context.ApplicationContext;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
import org.springframework.core.io.ClassPathResource;
10+
import org.springframework.web.reactive.config.ViewResolverRegistry;
11+
import org.springframework.web.reactive.config.WebFluxConfigurer;
12+
import org.springframework.web.reactive.function.server.RouterFunction;
13+
import org.springframework.web.reactive.function.server.RouterFunctions;
14+
import org.springframework.web.reactive.function.server.ServerResponse;
15+
import org.thymeleaf.spring5.SpringWebFluxTemplateEngine;
16+
import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver;
17+
import org.thymeleaf.templatemode.TemplateMode;
18+
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
19+
20+
import java.nio.charset.StandardCharsets;
21+
22+
@Configuration
23+
@ConditionalOnClass(WebFluxConfigurer.class)
24+
@ConditionalOnProperty(value = "graphql.playground.enabled", havingValue = "true", matchIfMissing = true)
25+
@RequiredArgsConstructor
26+
public class PlaygroundWebFluxAutoConfiguration implements WebFluxConfigurer {
27+
28+
private final ApplicationContext applicationContext;
29+
30+
@Bean
31+
public RouterFunction<ServerResponse> playgroundStaticFilesRouter() {
32+
return RouterFunctions.resources("/vendor/playground/**", new ClassPathResource("static/vendor/playground/"));
33+
}
34+
35+
@Override
36+
public void configureViewResolvers(final ViewResolverRegistry registry) {
37+
final ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
38+
templateResolver.setPrefix("templates/");
39+
templateResolver.setSuffix(".html");
40+
templateResolver.setTemplateMode(TemplateMode.HTML);
41+
templateResolver.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
42+
templateResolver.setOrder(1);
43+
templateResolver.setCheckExistence(true);
44+
final SpringWebFluxTemplateEngine springWebFluxTemplateEngine = new SpringWebFluxTemplateEngine();
45+
springWebFluxTemplateEngine.setTemplateResolver(templateResolver);
46+
final ThymeleafReactiveViewResolver thymeleafReactiveViewResolver = new ThymeleafReactiveViewResolver();
47+
thymeleafReactiveViewResolver.setDefaultCharset(StandardCharsets.UTF_8);
48+
thymeleafReactiveViewResolver.setApplicationContext(applicationContext);
49+
thymeleafReactiveViewResolver.setTemplateEngine(springWebFluxTemplateEngine);
50+
thymeleafReactiveViewResolver.setViewNames(new String[] {"playground"});
51+
registry.viewResolver(thymeleafReactiveViewResolver);
52+
}
53+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
org.springframework.boot.autoconfigure.EnableAutoConfiguration=graphql.kickstart.playground.boot.PlaygroundAutoConfiguration
1+
org.springframework.boot.autoconfigure.EnableAutoConfiguration=graphql.kickstart.playground.boot.PlaygroundAutoConfiguration,graphql.kickstart.playground.boot.PlaygroundWebFluxAutoConfiguration

playground-spring-boot-autoconfigure/src/main/resources/templates/playground.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html>
33

44
<head>
5-
<meta charset=utf-8/>
5+
<meta charset="utf-8"/>
66
<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
77
<title th:text="${pageTitle}"></title>
88
<link rel="stylesheet" th:href="${cssUrl}" />

playground-spring-boot-autoconfigure/src/test/java/graphql/kickstart/playground/boot/PlaygroundTestHelper.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55

66
import static org.assertj.core.api.Assertions.assertThat;
77

8-
final class PlaygroundTestHelper {
9-
static final String DEFAULT_PLAYGROUND_ENDPOINT = "/playground";
10-
static final String CSS_URL_FIELD_NAME = "cssUrl";
11-
static final String SCRIPT_URL_FIELD_NAME = "scriptUrl";
12-
static final String FAVICON_URL_FIELD_NAME = "faviconUrl";
13-
static final String LOGO_URL_FIELD_NAME = "logoUrl";
14-
static final String PAGE_TITLE_FIELD_NAME = "pageTitle";
8+
public final class PlaygroundTestHelper {
9+
public static final String DEFAULT_PLAYGROUND_ENDPOINT = "/playground";
10+
public static final String CSS_URL_FIELD_NAME = "cssUrl";
11+
public static final String SCRIPT_URL_FIELD_NAME = "scriptUrl";
12+
public static final String FAVICON_URL_FIELD_NAME = "faviconUrl";
13+
public static final String LOGO_URL_FIELD_NAME = "logoUrl";
14+
public static final String PAGE_TITLE_FIELD_NAME = "pageTitle";
1515

16-
static void assertTitle(final Document document, final String title) {
16+
public static void assertTitle(final Document document, final String title) {
1717
assertThat(document.select("head title")).extracting(Element::text).containsExactly(title);
1818
}
1919

@@ -29,7 +29,7 @@ private static void assertFavicon(final Document document, final String faviconU
2929
private static void assertLoadingLogo(final Document document, final String logoUrl) {
3030
assertThat(document.select(String.format("#root img[src=%s]", logoUrl)).size()).isOne();
3131
}
32-
static void assertStaticResources(final Document document, final String cssUrl, final String scriptUrl,
32+
public static void assertStaticResources(final Document document, final String cssUrl, final String scriptUrl,
3333
final String faviconUrl, final String logoUrl) {
3434
assertStylesheet(document, cssUrl);
3535
assertScript(document, scriptUrl);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package graphql.kickstart.playground.boot.webflux;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import graphql.kickstart.playground.boot.PlaygroundController;
5+
import graphql.kickstart.playground.boot.PlaygroundTestHelper;
6+
import org.jsoup.Jsoup;
7+
import org.jsoup.nodes.Document;
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
11+
import org.springframework.boot.test.context.SpringBootTest;
12+
import org.springframework.context.ApplicationContext;
13+
import org.springframework.core.io.ClassPathResource;
14+
import org.springframework.http.MediaType;
15+
import org.springframework.test.web.reactive.server.WebTestClient;
16+
import org.springframework.util.StreamUtils;
17+
import org.springframework.web.reactive.function.client.ExchangeStrategies;
18+
19+
import java.io.IOException;
20+
import java.nio.charset.StandardCharsets;
21+
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
24+
@SpringBootTest(classes = PlaygroundWebFluxTestConfig.class)
25+
@AutoConfigureWebTestClient
26+
public class PlaygroundWebFluxEnabledTest {
27+
28+
private static final int MAX_IN_MEMORY_SIZE = 3 * 1024 * 1024;
29+
private static final String DEFAULT_CSS_PATH = "/vendor/playground/static/css/index.css";
30+
private static final String DEFAULT_CSS_RESOURCE = "/static/vendor/playground/static/css/index.css";
31+
private static final String DEFAULT_SCRIPT_PATH = "/vendor/playground/static/js/middleware.js";
32+
private static final String DEFAULT_SCRIPT_RESOURCE = "/static/vendor/playground/static/js/middleware.js";
33+
private static final String DEFAULT_FAVICON_PATH = "/vendor/playground/favicon.png";
34+
private static final String DEFAULT_FAVICON_RESOURCE = "/static/vendor/playground/favicon.png";
35+
private static final String DEFAULT_LOGO_PATH = "/vendor/playground/logo.png";
36+
private static final String DEFAULT_LOGO_RESOURCE = "/static/vendor/playground/logo.png";
37+
private static final String DEFAULT_TITLE = "Playground";
38+
39+
@Autowired
40+
private ApplicationContext applicationContext;
41+
42+
@Autowired
43+
private WebTestClient webTestClient;
44+
45+
@Autowired
46+
private ObjectMapper objectMapper;
47+
48+
@Test
49+
public void playgroundLoads() {
50+
assertThat(applicationContext.getBean(PlaygroundController.class)).isNotNull();
51+
}
52+
53+
@Test
54+
public void playgroundShouldBeAvailableAtDefaultEndpoint() {
55+
// WHEN
56+
final byte[] content = webTestClient.get().uri(PlaygroundTestHelper.DEFAULT_PLAYGROUND_ENDPOINT)
57+
.accept(MediaType.TEXT_HTML)
58+
.acceptCharset(StandardCharsets.UTF_8)
59+
.exchange()
60+
.expectStatus().isOk()
61+
.expectHeader().contentTypeCompatibleWith(MediaType.TEXT_HTML)
62+
.expectBody()
63+
.returnResult()
64+
.getResponseBodyContent();
65+
// THEN
66+
assertThat(content).isNotNull();
67+
final Document document = Jsoup.parse(new String(content, StandardCharsets.UTF_8));
68+
PlaygroundTestHelper.assertTitle(document, DEFAULT_TITLE);
69+
PlaygroundTestHelper.assertStaticResources(document, DEFAULT_CSS_PATH, DEFAULT_SCRIPT_PATH, DEFAULT_FAVICON_PATH, DEFAULT_LOGO_PATH);
70+
}
71+
72+
@Test
73+
public void defaultCssShouldBeAvailable() throws IOException {
74+
testStaticResource(DEFAULT_CSS_RESOURCE, DEFAULT_CSS_PATH, "text/css");
75+
}
76+
77+
@Test
78+
public void defaultScriptShouldBeAvailable() throws Exception {
79+
testStaticResource(DEFAULT_SCRIPT_RESOURCE, DEFAULT_SCRIPT_PATH, "application/javascript");
80+
}
81+
82+
@Test
83+
public void defaultFaviconShouldBeAvailable() throws Exception {
84+
testStaticResource(DEFAULT_FAVICON_RESOURCE, DEFAULT_FAVICON_PATH, "image/png");
85+
}
86+
87+
@Test
88+
public void defaultLogoShouldBeAvailable() throws Exception {
89+
testStaticResource(DEFAULT_LOGO_RESOURCE, DEFAULT_LOGO_PATH, "image/png");
90+
}
91+
92+
private void testStaticResource(
93+
final String resourcePath,
94+
final String urlPath,
95+
final String contentType
96+
) throws IOException {
97+
// GIVEN
98+
final byte[] expected = StreamUtils.copyToByteArray(new ClassPathResource(resourcePath).getInputStream());
99+
// WHEN
100+
final byte[] actual = webTestClient
101+
.mutateWith((builder, httpHandlerBuilder, connector)
102+
-> builder.exchangeStrategies(ExchangeStrategies.builder().codecs(configurer
103+
-> configurer
104+
.defaultCodecs()
105+
.maxInMemorySize(MAX_IN_MEMORY_SIZE))
106+
.build()
107+
)
108+
)
109+
.get().uri(urlPath)
110+
.exchange()
111+
.expectStatus().isOk()
112+
.expectHeader().contentTypeCompatibleWith(contentType)
113+
.expectBody().returnResult().getResponseBody();
114+
// THEN
115+
assertThat(actual).isEqualTo(expected);
116+
}
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package graphql.kickstart.playground.boot.webflux;
2+
3+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
6+
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
9+
import org.springframework.security.config.web.server.ServerHttpSecurity;
10+
import org.springframework.security.web.server.SecurityWebFilterChain;
11+
import org.springframework.web.reactive.config.EnableWebFlux;
12+
13+
@EnableWebFlux
14+
@EnableWebFluxSecurity
15+
@SpringBootApplication
16+
@EnableAutoConfiguration(exclude = { WebMvcAutoConfiguration.class, SecurityAutoConfiguration.class } )
17+
class PlaygroundWebFluxTestConfig {
18+
19+
@Bean
20+
public SecurityWebFilterChain securityWebFilterChain(final ServerHttpSecurity http) {
21+
return http.authorizeExchange().anyExchange().permitAll().and().build();
22+
}
23+
}

0 commit comments

Comments
 (0)