Turn off auto-formatting where it reduces readability
This commit is contained in:
@@ -45,13 +45,15 @@ import org.springframework.graphql.web.webflux.GraphQlWebSocketHandler;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.function.server.RequestPredicates;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.web.reactive.socket.server.support.WebSocketUpgradeHandlerPredicate;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
|
||||
* WebFlux.
|
||||
@@ -90,15 +92,18 @@ public class WebFluxGraphQlAutoConfiguration {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("GraphQL endpoint HTTP POST " + path);
|
||||
}
|
||||
// @formatter:off
|
||||
RouterFunctions.Builder builder = RouterFunctions.route()
|
||||
.GET(path, (req) -> ServerResponse.ok().bodyValue(resource))
|
||||
.POST(path, RequestPredicates.accept(MediaType.APPLICATION_JSON)
|
||||
.and(RequestPredicates.contentType(MediaType.APPLICATION_JSON)), handler::handleRequest);
|
||||
.POST(path, accept(MediaType.APPLICATION_JSON).and(contentType(MediaType.APPLICATION_JSON)), handler::handleRequest);
|
||||
if (properties.getSchema().getPrinter().isEnabled()) {
|
||||
SchemaPrinter printer = new SchemaPrinter();
|
||||
builder = builder.GET(path + properties.getSchema().getPrinter().getPath(), (req) -> ServerResponse.ok()
|
||||
.contentType(MediaType.TEXT_PLAIN).bodyValue(printer.print(graphQlSource.schema())));
|
||||
builder = builder.GET(path + properties.getSchema().getPrinter().getPath(),
|
||||
(req) -> ServerResponse.ok()
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.bodyValue(printer.print(graphQlSource.schema())));
|
||||
}
|
||||
// @formatter:on
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ import org.springframework.graphql.web.webmvc.GraphQlWebSocketHandler;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.function.RequestPredicates;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.RouterFunctions;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
@@ -59,6 +58,9 @@ import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
|
||||
import org.springframework.web.socket.server.support.WebSocketHandlerMapping;
|
||||
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
|
||||
|
||||
import static org.springframework.web.servlet.function.RequestPredicates.accept;
|
||||
import static org.springframework.web.servlet.function.RequestPredicates.contentType;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
|
||||
* Spring MVC.
|
||||
@@ -100,14 +102,18 @@ public class WebMvcGraphQlAutoConfiguration {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("GraphQL endpoint HTTP POST " + path);
|
||||
}
|
||||
RouterFunctions.Builder builder = RouterFunctions.route().GET(path, (req) -> ServerResponse.ok().body(resource))
|
||||
.POST(path, RequestPredicates.contentType(MediaType.APPLICATION_JSON)
|
||||
.and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::handleRequest);
|
||||
// @formatter:off
|
||||
RouterFunctions.Builder builder = RouterFunctions.route()
|
||||
.GET(path, (req) -> ServerResponse.ok().body(resource))
|
||||
.POST(path, contentType(MediaType.APPLICATION_JSON).and(accept(MediaType.APPLICATION_JSON)), handler::handleRequest);
|
||||
if (properties.getSchema().getPrinter().isEnabled()) {
|
||||
SchemaPrinter printer = new SchemaPrinter();
|
||||
builder = builder.GET(path + properties.getSchema().getPrinter().getPath(), (req) -> ServerResponse.ok()
|
||||
.contentType(MediaType.TEXT_PLAIN).body(printer.print(graphQlSource.schema())));
|
||||
builder = builder.GET(path + properties.getSchema().getPrinter().getPath(),
|
||||
(req) -> ServerResponse.ok()
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.body(printer.print(graphQlSource.schema())));
|
||||
}
|
||||
// @formatter:on
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@@ -121,9 +127,12 @@ public class WebMvcGraphQlAutoConfiguration {
|
||||
public GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
|
||||
GraphQlProperties properties, HttpMessageConverters converters) {
|
||||
|
||||
// @formatter:off
|
||||
HttpMessageConverter<?> converter = converters.getConverters().stream()
|
||||
.filter((candidate) -> candidate.canRead(Map.class, MediaType.APPLICATION_JSON)).findFirst()
|
||||
.filter((candidate) -> candidate.canRead(Map.class, MediaType.APPLICATION_JSON))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("No JSON converter"));
|
||||
// @formatter:on
|
||||
|
||||
return new GraphQlWebSocketHandler(webGraphQlHandler, converter,
|
||||
properties.getWebsocket().getConnectionInitTimeout());
|
||||
|
||||
@@ -5,7 +5,6 @@ import java.util.Map;
|
||||
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.graphql.boot.RuntimeWiringCustomizer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import graphql.ErrorClassification;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import graphql.GraphQLError;
|
||||
import graphql.GraphqlErrorBuilder;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
|
||||
import org.springframework.graphql.execution.ErrorType;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
@@ -13,19 +17,18 @@ import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
// @formatter:off
|
||||
|
||||
@Component
|
||||
public class SecurityDataFetcherExceptionResolver implements DataFetcherExceptionResolver {
|
||||
|
||||
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
|
||||
|
||||
@Override
|
||||
public Mono<List<GraphQLError>> resolveException(Throwable exception, DataFetchingEnvironment environment) {
|
||||
if (exception instanceof AuthenticationException) {
|
||||
|
||||
// TOTO: should this be empty ?
|
||||
}
|
||||
if (exception instanceof AccessDeniedException) {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
@@ -38,10 +41,19 @@ public class SecurityDataFetcherExceptionResolver implements DataFetcherExceptio
|
||||
}
|
||||
|
||||
private Mono<List<GraphQLError>> unauthorized(DataFetchingEnvironment environment) {
|
||||
return Mono.fromCallable(() -> Arrays.asList(GraphqlErrorBuilder.newError(environment).errorType(ErrorType.UNAUTHORIZED).message("Unauthorized").build()));
|
||||
return Mono.fromCallable(() -> Arrays.asList(
|
||||
GraphqlErrorBuilder.newError(environment)
|
||||
.errorType(ErrorType.UNAUTHORIZED)
|
||||
.message("Unauthorized")
|
||||
.build()));
|
||||
}
|
||||
|
||||
private Mono<List<GraphQLError>> forbidden(DataFetchingEnvironment environment) {
|
||||
return Mono.fromCallable(() -> Arrays.asList(GraphqlErrorBuilder.newError(environment).errorType(ErrorType.FORBIDDEN).message("Forbidden").build()));
|
||||
return Mono.fromCallable(() -> Arrays.asList(
|
||||
GraphqlErrorBuilder.newError(environment)
|
||||
.errorType(ErrorType.FORBIDDEN)
|
||||
.message("Forbidden")
|
||||
.build()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext;
|
||||
@@ -10,7 +13,7 @@ import org.springframework.security.test.web.reactive.server.SecurityMockServerC
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions;
|
||||
|
||||
import java.util.Collections;
|
||||
// @formatter:off
|
||||
|
||||
@SpringBootTest()
|
||||
class SampleApplicationTests {
|
||||
|
||||
@@ -24,24 +24,20 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
public class SampleWiring implements RuntimeWiringCustomizer {
|
||||
|
||||
private final DataRepository dataRepository;
|
||||
private final DataRepository repository;
|
||||
|
||||
public SampleWiring(@Autowired DataRepository dataRepository) {
|
||||
this.dataRepository = dataRepository;
|
||||
this.repository = dataRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(RuntimeWiring.Builder builder) {
|
||||
|
||||
builder.type("Query", typeBuilder -> typeBuilder.dataFetcher("greeting", this.dataRepository::getBasic));
|
||||
|
||||
builder.type("Query", typeBuilder -> typeBuilder.dataFetcher("greetingMono", this.dataRepository::getGreeting));
|
||||
|
||||
builder.type("Query",
|
||||
typeBuilder -> typeBuilder.dataFetcher("greetingsFlux", this.dataRepository::getGreetings));
|
||||
|
||||
builder.type("Subscription",
|
||||
typeBuilder -> typeBuilder.dataFetcher("greetings", this.dataRepository::getGreetingsStream));
|
||||
public void customize(RuntimeWiring.Builder wiringBuilder) {
|
||||
// @formatter:off
|
||||
wiringBuilder.type("Query", builder -> builder.dataFetcher("greeting", this.repository::getBasic));
|
||||
wiringBuilder.type("Query", builder -> builder.dataFetcher("greetingMono", this.repository::getGreeting));
|
||||
wiringBuilder.type("Query", builder -> builder.dataFetcher("greetingsFlux", this.repository::getGreetings));
|
||||
wiringBuilder.type("Subscription", builder -> builder.dataFetcher("greetings", this.repository::getGreetingsStream));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,8 +21,10 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||
import org.springframework.graphql.test.tester.GraphQlTester;
|
||||
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||
|
||||
// @formatter:off
|
||||
|
||||
/**
|
||||
* GraphQL query tests directly via {@link GraphQL}.
|
||||
@@ -34,19 +36,25 @@ public class QueryTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setUp(@Autowired WebGraphQlHandler handler) {
|
||||
this.graphQlTester = GraphQlTester
|
||||
.create(webInput -> handler.handle(webInput).contextWrite(context -> context.put("name", "James")));
|
||||
this.graphQlTester = GraphQlTester.create(webInput ->
|
||||
handler.handle(webInput).contextWrite(context -> context.put("name", "James")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void greetingMono() {
|
||||
this.graphQlTester.query("{greetingMono}").execute().path("greetingMono").entity(String.class)
|
||||
this.graphQlTester.query("{greetingMono}")
|
||||
.execute()
|
||||
.path("greetingMono")
|
||||
.entity(String.class)
|
||||
.isEqualTo("Hello James");
|
||||
}
|
||||
|
||||
@Test
|
||||
void greetingsFlux() {
|
||||
this.graphQlTester.query("{greetingsFlux}").execute().path("greetingsFlux").entityList(String.class)
|
||||
this.graphQlTester.query("{greetingsFlux}")
|
||||
.execute()
|
||||
.path("greetingsFlux")
|
||||
.entityList(String.class)
|
||||
.containsExactly("Hi James", "Bonjour James", "Hola James", "Ciao James", "Zdravo James");
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,10 @@ import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||
import org.springframework.graphql.test.tester.GraphQlTester;
|
||||
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||
|
||||
// @formatter:off
|
||||
|
||||
/**
|
||||
* GraphQL subscription tests directly via {@link GraphQL}.
|
||||
@@ -36,29 +38,36 @@ public class SubscriptionTests {
|
||||
|
||||
@BeforeEach
|
||||
public void setUp(@Autowired WebGraphQlHandler handler) {
|
||||
this.graphQlTester = GraphQlTester
|
||||
.create(webInput -> handler.handle(webInput).contextWrite(context -> context.put("name", "James")));
|
||||
this.graphQlTester = GraphQlTester.create(webInput ->
|
||||
handler.handle(webInput).contextWrite(context -> context.put("name", "James")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void subscriptionWithEntityPath() {
|
||||
String query = "subscription { greetings }";
|
||||
Flux<String> result = this.graphQlTester.query("subscription { greetings }")
|
||||
.executeSubscription()
|
||||
.toFlux("greetings", String.class);
|
||||
|
||||
Flux<String> result = this.graphQlTester.query(query).executeSubscription().toFlux("greetings", String.class);
|
||||
|
||||
StepVerifier.create(result).expectNext("Hi James").expectNext("Bonjour James").expectNext("Hola James")
|
||||
.expectNext("Ciao James").expectNext("Zdravo James").verifyComplete();
|
||||
StepVerifier.create(result)
|
||||
.expectNext("Hi James")
|
||||
.expectNext("Bonjour James")
|
||||
.expectNext("Hola James")
|
||||
.expectNext("Ciao James")
|
||||
.expectNext("Zdravo James")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void subscriptionWithResponseSpec() {
|
||||
String query = "subscription { greetings }";
|
||||
Flux<GraphQlTester.ResponseSpec> result = this.graphQlTester.query("subscription { greetings }")
|
||||
.executeSubscription()
|
||||
.toFlux();
|
||||
|
||||
Flux<GraphQlTester.ResponseSpec> result = this.graphQlTester.query(query).executeSubscription().toFlux();
|
||||
|
||||
StepVerifier.create(result).consumeNextWith(spec -> spec.path("greetings").valueExists())
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(spec -> spec.path("greetings").valueExists())
|
||||
.consumeNextWith(spec -> spec.path("greetings").matchesJson("\"Bonjour James\""))
|
||||
.consumeNextWith(spec -> spec.path("greetings").matchesJson("\"Hola James\"")).expectNextCount(2)
|
||||
.consumeNextWith(spec -> spec.path("greetings").matchesJson("\"Hola James\""))
|
||||
.expectNextCount(2)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,12 @@ public enum ProjectStatus {
|
||||
|
||||
@JsonCreator
|
||||
public static ProjectStatus fromName(String name) {
|
||||
return Arrays.stream(ProjectStatus.values()).filter(type -> type.name().equals(name)).findFirst()
|
||||
// @formatter:off
|
||||
return Arrays.stream(ProjectStatus.values())
|
||||
.filter(type -> type.name().equals(name))
|
||||
.findFirst()
|
||||
.orElse(ProjectStatus.ACTIVE);
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,8 +10,12 @@ public enum ReleaseStatus {
|
||||
|
||||
@JsonCreator
|
||||
public static ReleaseStatus fromName(String name) {
|
||||
return Arrays.stream(ReleaseStatus.values()).filter(type -> type.name().equals(name)).findFirst()
|
||||
// @formatter:off
|
||||
return Arrays.stream(ReleaseStatus.values())
|
||||
.filter(type -> type.name().equals(name))
|
||||
.findFirst()
|
||||
.orElse(ReleaseStatus.GENERAL_AVAILABILITY);
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@ package io.spring.sample.graphql.project;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.hateoas.CollectionModel;
|
||||
@@ -13,27 +10,35 @@ import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.client.Hop;
|
||||
import org.springframework.hateoas.client.Traverson;
|
||||
import org.springframework.hateoas.server.core.TypeReferences;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Component
|
||||
public class SpringProjectsClient {
|
||||
|
||||
private static final TypeReferences.CollectionModelType<Release> releaseCollection = new TypeReferences.CollectionModelType<Release>() {
|
||||
};
|
||||
// @formatter:off
|
||||
|
||||
private static final TypeReferences.CollectionModelType<Release> releaseCollection =
|
||||
new TypeReferences.CollectionModelType<Release>() {};
|
||||
|
||||
// @formatter:on
|
||||
|
||||
private final Traverson traverson;
|
||||
|
||||
public SpringProjectsClient(RestTemplateBuilder builder) {
|
||||
RestTemplate restTemplate = builder
|
||||
.messageConverters(Traverson.getDefaultMessageConverters(MediaTypes.HAL_JSON)).build();
|
||||
List<HttpMessageConverter<?>> converters = Traverson.getDefaultMessageConverters(MediaTypes.HAL_JSON);
|
||||
RestTemplate restTemplate = builder.messageConverters(converters).build();
|
||||
this.traverson = new Traverson(URI.create("https://spring.io/api/"), MediaTypes.HAL_JSON);
|
||||
this.traverson.setRestOperations(restTemplate);
|
||||
}
|
||||
|
||||
public Project fetchProject(String projectSlug) {
|
||||
return this.traverson.follow("projects").follow(Hop.rel("project").withParameter("id", projectSlug))
|
||||
// @formatter:off
|
||||
return this.traverson.follow("projects")
|
||||
.follow(Hop.rel("project").withParameter("id", projectSlug))
|
||||
.toObject(Project.class);
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
public List<Release> fetchProjectReleases(String projectSlug) {
|
||||
|
||||
@@ -18,12 +18,12 @@ public class ArtifactRepositoriesInitializer implements ApplicationRunner {
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
// @formatter:off
|
||||
List<ArtifactRepository> repositoryList = Arrays.asList(
|
||||
new ArtifactRepository("spring-releases", "Spring Releases", "https://repo.spring.io/libs-releases"),
|
||||
new ArtifactRepository("spring-milestones", "Spring Milestones",
|
||||
"https://repo.spring.io/libs-milestones"),
|
||||
new ArtifactRepository("spring-snapshots", "Spring Snapshots",
|
||||
"https://repo.spring.io/libs-snapshots"));
|
||||
new ArtifactRepository("spring-milestones", "Spring Milestones", "https://repo.spring.io/libs-milestones"),
|
||||
new ArtifactRepository("spring-snapshots", "Spring Snapshots", "https://repo.spring.io/libs-snapshots"));
|
||||
// @formatter:on
|
||||
repositories.saveAll(repositoryList);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
// @formatter:off
|
||||
|
||||
/**
|
||||
* GraphQL requests via {@link GraphQlTester} connecting to {@link MockMvc}.
|
||||
*/
|
||||
@@ -39,28 +41,50 @@ public class MockMvcGraphQlTests {
|
||||
|
||||
@Test
|
||||
void jsonPath() {
|
||||
String query = "{" + " project(slug:\"spring-framework\") {" + " releases {" + " version" + " }"
|
||||
+ " }" + "}";
|
||||
String query = "{" +
|
||||
" project(slug:\"spring-framework\") {" +
|
||||
" releases {" +
|
||||
" version" +
|
||||
" }"+
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
this.graphQlTester.query(query).execute().path("project.releases[*].version").entityList(String.class)
|
||||
this.graphQlTester.query(query)
|
||||
.execute()
|
||||
.path("project.releases[*].version")
|
||||
.entityList(String.class)
|
||||
.hasSizeGreaterThan(1);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonContent() {
|
||||
String query = "{" + " project(slug:\"spring-framework\") {" + " repositoryUrl" + " }" + "}";
|
||||
String query = "{" +
|
||||
" project(slug:\"spring-framework\") {" +
|
||||
" repositoryUrl" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
this.graphQlTester.query(query).execute().path("project")
|
||||
this.graphQlTester.query(query)
|
||||
.execute()
|
||||
.path("project")
|
||||
.matchesJson("{\"repositoryUrl\":\"http://github.com/spring-projects/spring-framework\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void decodedResponse() {
|
||||
String query = "{" + " project(slug:\"spring-framework\") {" + " releases {" + " version" + " }"
|
||||
+ " }" + "}";
|
||||
String query = "{" +
|
||||
" project(slug:\"spring-framework\") {" +
|
||||
" releases {" +
|
||||
" version" +
|
||||
" }" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
this.graphQlTester.query(query).execute().path("project").entity(Project.class)
|
||||
this.graphQlTester.query(query)
|
||||
.execute()
|
||||
.path("project")
|
||||
.entity(Project.class)
|
||||
.satisfies(project -> assertThat(project.getReleases()).hasSizeGreaterThan(1));
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ import org.springframework.web.client.ExtractingResponseErrorHandler;
|
||||
*/
|
||||
class ExceptionResolversExceptionHandler implements DataFetcherExceptionHandler {
|
||||
|
||||
private static Log logger = LogFactory.getLog(ExtractingResponseErrorHandler.class);
|
||||
private static final Log logger = LogFactory.getLog(ExtractingResponseErrorHandler.class);
|
||||
|
||||
private final List<DataFetcherExceptionResolver> resolvers;
|
||||
|
||||
@@ -63,17 +63,23 @@ class ExceptionResolversExceptionHandler implements DataFetcherExceptionHandler
|
||||
return invokeChain(exception, parameters.getDataFetchingEnvironment());
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
// @formatter:off
|
||||
|
||||
DataFetcherExceptionHandlerResult invokeChain(Throwable ex, DataFetchingEnvironment env) {
|
||||
// For now we have to block:
|
||||
// https://github.com/graphql-java/graphql-java/issues/2356
|
||||
try {
|
||||
return Flux.fromIterable(this.resolvers).flatMap((resolver) -> resolver.resolveException(ex, env)).next()
|
||||
return Flux.fromIterable(this.resolvers)
|
||||
.flatMap((resolver) -> resolver.resolveException(ex, env))
|
||||
.next()
|
||||
.map((errors) -> DataFetcherExceptionHandlerResult.newResult().errors(errors).build())
|
||||
.switchIfEmpty(Mono.fromCallable(() -> applyDefaultHandling(ex, env))).contextWrite((context) -> {
|
||||
.switchIfEmpty(Mono.fromCallable(() -> applyDefaultHandling(ex, env)))
|
||||
.contextWrite((context) -> {
|
||||
ContextView contextView = ContextManager.getReactorContext(env);
|
||||
return (contextView.isEmpty() ? context : context.putAll(contextView));
|
||||
}).toFuture().get();
|
||||
})
|
||||
.toFuture()
|
||||
.get();
|
||||
}
|
||||
catch (Exception ex2) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
@@ -84,9 +90,13 @@ class ExceptionResolversExceptionHandler implements DataFetcherExceptionHandler
|
||||
}
|
||||
|
||||
private DataFetcherExceptionHandlerResult applyDefaultHandling(Throwable ex, DataFetchingEnvironment env) {
|
||||
GraphQLError error = GraphqlErrorBuilder.newError(env).message(ex.getMessage())
|
||||
.errorType(ErrorType.INTERNAL_ERROR).build();
|
||||
GraphQLError error = GraphqlErrorBuilder.newError(env)
|
||||
.message(ex.getMessage())
|
||||
.errorType(ErrorType.INTERNAL_ERROR)
|
||||
.build();
|
||||
return DataFetcherExceptionHandlerResult.newResult(error).build();
|
||||
}
|
||||
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
||||
@@ -90,9 +90,12 @@ class DefaultWebGraphQlHandlerBuilder implements WebGraphQlHandler.Builder {
|
||||
return this.service.execute(executionInput).map((result) -> new WebOutput(webInput, result));
|
||||
};
|
||||
|
||||
WebGraphQlHandler interceptionChain = interceptorsToUse.stream().reduce(WebInterceptor::andThen)
|
||||
// @formatter:off
|
||||
WebGraphQlHandler interceptionChain = interceptorsToUse.stream()
|
||||
.reduce(WebInterceptor::andThen)
|
||||
.map((interceptor) -> (WebGraphQlHandler) (input) -> interceptor.intercept(input, targetHandler))
|
||||
.orElse(targetHandler);
|
||||
// @formatter:on
|
||||
|
||||
return (CollectionUtils.isEmpty(this.accessors) ? interceptionChain
|
||||
: new ThreadLocalExtractingHandler(interceptionChain, ThreadLocalAccessor.composite(this.accessors)));
|
||||
|
||||
@@ -39,8 +39,10 @@ public class GraphQlHttpHandler {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GraphQlHttpHandler.class);
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> MAP_PARAMETERIZED_TYPE_REF = new ParameterizedTypeReference<Map<String, Object>>() {
|
||||
};
|
||||
// @formatter:off
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> MAP_PARAMETERIZED_TYPE_REF =
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
// @formatter:on
|
||||
|
||||
private final WebGraphQlHandler graphQlHandler;
|
||||
|
||||
|
||||
@@ -69,12 +69,15 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GraphQlWebSocketHandler.class);
|
||||
|
||||
private static final List<String> SUB_PROTOCOL_LIST = Arrays.asList("graphql-transport-ws",
|
||||
"subscriptions-transport-ws");
|
||||
// @formatter:off
|
||||
|
||||
static final ResolvableType MAP_RESOLVABLE_TYPE = ResolvableType
|
||||
.forType(new ParameterizedTypeReference<Map<String, Object>>() {
|
||||
});
|
||||
private static final List<String> SUB_PROTOCOL_LIST =
|
||||
Arrays.asList("graphql-transport-ws", "subscriptions-transport-ws");
|
||||
|
||||
static final ResolvableType MAP_RESOLVABLE_TYPE =
|
||||
ResolvableType.forType(new ParameterizedTypeReference<Map<String, Object>>() {});
|
||||
|
||||
// @formatter:off
|
||||
|
||||
private final WebGraphQlHandler graphQlHandler;
|
||||
|
||||
@@ -101,20 +104,26 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
|
||||
this.initTimeoutDuration = connectionInitTimeout;
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
|
||||
private static Decoder<?> initDecoder(ServerCodecConfigurer configurer) {
|
||||
return configurer.getReaders().stream()
|
||||
.filter((reader) -> reader.canRead(MAP_RESOLVABLE_TYPE, MediaType.APPLICATION_JSON))
|
||||
.map((reader) -> ((DecoderHttpMessageReader<?>) reader).getDecoder()).findFirst()
|
||||
.map((reader) -> ((DecoderHttpMessageReader<?>) reader).getDecoder())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("No JSON Decoder"));
|
||||
}
|
||||
|
||||
private static Encoder<?> initEncoder(ServerCodecConfigurer configurer) {
|
||||
return configurer.getWriters().stream()
|
||||
.filter((writer) -> writer.canWrite(MAP_RESOLVABLE_TYPE, MediaType.APPLICATION_JSON))
|
||||
.map((writer) -> ((EncoderHttpMessageWriter<?>) writer).getEncoder()).findFirst()
|
||||
.map((writer) -> ((EncoderHttpMessageWriter<?>) writer).getEncoder())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("No JSON Encoder"));
|
||||
}
|
||||
|
||||
// @formatter:on
|
||||
|
||||
@Override
|
||||
public List<String> getSubProtocols() {
|
||||
return SUB_PROTOCOL_LIST;
|
||||
@@ -135,8 +144,16 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
|
||||
AtomicBoolean connectionInitProcessed = new AtomicBoolean();
|
||||
Map<String, Subscription> subscriptions = new ConcurrentHashMap<>();
|
||||
|
||||
Mono.delay(this.initTimeoutDuration).then(Mono.defer(() -> connectionInitProcessed.compareAndSet(false, true)
|
||||
? session.close(GraphQlStatus.INIT_TIMEOUT_STATUS) : Mono.empty())).subscribe();
|
||||
// @formatter:off
|
||||
|
||||
Mono.delay(this.initTimeoutDuration)
|
||||
.then(Mono.defer(() ->
|
||||
connectionInitProcessed.compareAndSet(false, true) ?
|
||||
session.close(GraphQlStatus.INIT_TIMEOUT_STATUS) :
|
||||
Mono.empty()))
|
||||
.subscribe();
|
||||
|
||||
// @formatter:on
|
||||
|
||||
return session.send(session.receive().flatMap((message) -> {
|
||||
Map<String, Object> map = decode(message);
|
||||
@@ -192,6 +209,8 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
|
||||
return payload;
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Flux<WebSocketMessage> handleWebOutput(WebSocketSession session, String id,
|
||||
Map<String, Subscription> subscriptions, WebOutput output) {
|
||||
@@ -205,35 +224,42 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
|
||||
Flux<ExecutionResult> outputFlux;
|
||||
if (output.getData() instanceof Publisher) {
|
||||
// Subscription
|
||||
outputFlux = Flux.from((Publisher<ExecutionResult>) output.getData()).doOnSubscribe((subscription) -> {
|
||||
Subscription previous = subscriptions.putIfAbsent(id, subscription);
|
||||
if (previous != null) {
|
||||
throw new SubscriptionExistsException();
|
||||
}
|
||||
});
|
||||
outputFlux = Flux.from((Publisher<ExecutionResult>) output.getData())
|
||||
.doOnSubscribe((subscription) -> {
|
||||
Subscription previous = subscriptions.putIfAbsent(id, subscription);
|
||||
if (previous != null) {
|
||||
throw new SubscriptionExistsException();
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Single response operation (query or mutation)
|
||||
outputFlux = (CollectionUtils.isEmpty(output.getErrors()) ? Flux.just(output)
|
||||
: Flux.error(new IllegalStateException("Execution failed: " + output.getErrors())));
|
||||
outputFlux = (CollectionUtils.isEmpty(output.getErrors()) ? Flux.just(output) :
|
||||
Flux.error(new IllegalStateException("Execution failed: " + output.getErrors())));
|
||||
}
|
||||
|
||||
return outputFlux.map((result) -> {
|
||||
Map<String, Object> dataMap = result.toSpecification();
|
||||
return encode(session, id, MessageType.NEXT, dataMap);
|
||||
}).concatWith(Mono.fromCallable(() -> encode(session, id, MessageType.COMPLETE, null))).onErrorResume((ex) -> {
|
||||
if (ex instanceof SubscriptionExistsException) {
|
||||
CloseStatus status = new CloseStatus(4409, "Subscriber for " + id + " already exists");
|
||||
return GraphQlStatus.close(session, status);
|
||||
}
|
||||
ErrorType errorType = ErrorType.DataFetchingException;
|
||||
String message = ex.getMessage();
|
||||
Map<String, Object> errorMap = GraphqlErrorBuilder.newError().errorType(errorType).message(message).build()
|
||||
.toSpecification();
|
||||
return Mono.just(encode(session, id, MessageType.ERROR, errorMap));
|
||||
});
|
||||
return outputFlux
|
||||
.map((result) -> {
|
||||
Map<String, Object> dataMap = result.toSpecification();
|
||||
return encode(session, id, MessageType.NEXT, dataMap);
|
||||
})
|
||||
.concatWith(Mono.fromCallable(() -> encode(session, id, MessageType.COMPLETE, null)))
|
||||
.onErrorResume((ex) -> {
|
||||
if (ex instanceof SubscriptionExistsException) {
|
||||
CloseStatus status = new CloseStatus(4409, "Subscriber for " + id + " already exists");
|
||||
return GraphQlStatus.close(session, status);
|
||||
}
|
||||
Map<String, Object> errorMap = GraphqlErrorBuilder.newError()
|
||||
.errorType(ErrorType.DataFetchingException)
|
||||
.message(ex.getMessage())
|
||||
.build()
|
||||
.toSpecification();
|
||||
return Mono.just(encode(session, id, MessageType.ERROR, errorMap));
|
||||
});
|
||||
}
|
||||
|
||||
// @formatter:on
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> WebSocketMessage encode(WebSocketSession session, @Nullable String id, MessageType messageType,
|
||||
@Nullable Object payload) {
|
||||
@@ -255,8 +281,16 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
|
||||
|
||||
private enum MessageType {
|
||||
|
||||
CONNECTION_INIT("connection_init"), CONNECTION_ACK("connection_ack"), SUBSCRIBE("subscribe"), NEXT(
|
||||
"next"), ERROR("error"), COMPLETE("complete");
|
||||
// @formatter:off
|
||||
|
||||
CONNECTION_INIT("connection_init"),
|
||||
CONNECTION_ACK("connection_ack"),
|
||||
SUBSCRIBE("subscribe"),
|
||||
NEXT("next"),
|
||||
ERROR("error"),
|
||||
COMPLETE("complete");
|
||||
|
||||
// @formatter:on
|
||||
|
||||
private static final Map<String, MessageType> messageTypes = new HashMap<>(6);
|
||||
|
||||
@@ -285,14 +319,17 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
|
||||
|
||||
private static class GraphQlStatus {
|
||||
|
||||
// @formatter:off
|
||||
|
||||
static final CloseStatus INVALID_MESSAGE_STATUS = new CloseStatus(4400, "Invalid message");
|
||||
|
||||
static final CloseStatus UNAUTHORIZED_STATUS = new CloseStatus(4401, "Unauthorized");
|
||||
|
||||
static final CloseStatus INIT_TIMEOUT_STATUS = new CloseStatus(4408, "Connection initialisation timeout");
|
||||
|
||||
static final CloseStatus TOO_MANY_INIT_REQUESTS_STATUS = new CloseStatus(4429,
|
||||
"Too many initialisation requests");
|
||||
static final CloseStatus TOO_MANY_INIT_REQUESTS_STATUS = new CloseStatus(4429, "Too many initialisation requests");
|
||||
|
||||
// @formatter:on
|
||||
|
||||
static <V> Flux<V> close(WebSocketSession session, CloseStatus status) {
|
||||
return session.close(status).thenMany(Mono.empty());
|
||||
|
||||
@@ -46,8 +46,10 @@ public class GraphQlHttpHandler {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GraphQlHttpHandler.class);
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> MAP_PARAMETERIZED_TYPE_REF = new ParameterizedTypeReference<Map<String, Object>>() {
|
||||
};
|
||||
// @formatter:off
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> MAP_PARAMETERIZED_TYPE_REF =
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
// @formatter:on
|
||||
|
||||
private final WebGraphQlHandler graphQlHandler;
|
||||
|
||||
|
||||
@@ -72,8 +72,12 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GraphQlWebSocketHandler.class);
|
||||
|
||||
private static final List<String> SUB_PROTOCOL_LIST = Arrays.asList("graphql-transport-ws",
|
||||
"subscriptions-transport-ws");
|
||||
// @formatter:off
|
||||
|
||||
private static final List<String> SUB_PROTOCOL_LIST =
|
||||
Arrays.asList("graphql-transport-ws", "subscriptions-transport-ws");
|
||||
|
||||
// @formatter:on
|
||||
|
||||
private final WebGraphQlHandler graphQlHandler;
|
||||
|
||||
@@ -119,11 +123,18 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub
|
||||
SessionState sessionState = new SessionState(session.getId());
|
||||
this.sessionInfoMap.put(session.getId(), sessionState);
|
||||
|
||||
Mono.delay(this.initTimeoutDuration).then(Mono.fromRunnable(() -> {
|
||||
if (sessionState.isConnectionInitNotProcessed()) {
|
||||
GraphQlStatus.closeSession(session, GraphQlStatus.INIT_TIMEOUT_STATUS);
|
||||
}
|
||||
})).subscribe();
|
||||
// @formatter:off
|
||||
|
||||
Mono.delay(this.initTimeoutDuration)
|
||||
.then(Mono.fromRunnable(() -> {
|
||||
if (sessionState.isConnectionInitNotProcessed()) {
|
||||
GraphQlStatus.closeSession(session, GraphQlStatus.INIT_TIMEOUT_STATUS);
|
||||
}
|
||||
}))
|
||||
.subscribe();
|
||||
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -154,10 +165,12 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Executing: " + input);
|
||||
}
|
||||
this.graphQlHandler.handle(input).flatMapMany((output) -> handleWebOutput(session, input.getId(), output))
|
||||
.publishOn(sessionState.getScheduler()) // Serial blocking send via
|
||||
// single thread
|
||||
// @formatter:off
|
||||
this.graphQlHandler.handle(input)
|
||||
.flatMapMany((output) -> handleWebOutput(session, input.getId(), output))
|
||||
.publishOn(sessionState.getScheduler()) // Serial blocking send via single thread
|
||||
.subscribe(new SendMessageSubscriber(id, session, sessionState));
|
||||
// @formatter:on
|
||||
return;
|
||||
case COMPLETE:
|
||||
if (id != null) {
|
||||
@@ -199,6 +212,8 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub
|
||||
return info;
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Flux<TextMessage> handleWebOutput(WebSocketSession session, String id, WebOutput output) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
@@ -209,12 +224,13 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub
|
||||
Flux<ExecutionResult> outputFlux;
|
||||
if (output.getData() instanceof Publisher) {
|
||||
// Subscription
|
||||
outputFlux = Flux.from((Publisher<ExecutionResult>) output.getData()).doOnSubscribe((subscription) -> {
|
||||
Subscription prev = getSessionInfo(session).getSubscriptions().putIfAbsent(id, subscription);
|
||||
if (prev != null) {
|
||||
throw new SubscriptionExistsException();
|
||||
}
|
||||
});
|
||||
outputFlux = Flux.from((Publisher<ExecutionResult>) output.getData())
|
||||
.doOnSubscribe((subscription) -> {
|
||||
Subscription prev = getSessionInfo(session).getSubscriptions().putIfAbsent(id, subscription);
|
||||
if (prev != null) {
|
||||
throw new SubscriptionExistsException();
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Single response operation (query or mutation)
|
||||
@@ -222,23 +238,28 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub
|
||||
: Flux.error(new IllegalStateException("Execution failed: " + output.getErrors())));
|
||||
}
|
||||
|
||||
return outputFlux.map((result) -> {
|
||||
Map<String, Object> dataMap = result.toSpecification();
|
||||
return encode(id, MessageType.NEXT, dataMap);
|
||||
}).concatWith(Mono.fromCallable(() -> encode(id, MessageType.COMPLETE, null))).onErrorResume((ex) -> {
|
||||
if (ex instanceof SubscriptionExistsException) {
|
||||
CloseStatus status = new CloseStatus(4409, "Subscriber for " + id + " already exists");
|
||||
GraphQlStatus.closeSession(session, status);
|
||||
return Flux.empty();
|
||||
}
|
||||
ErrorType errorType = ErrorType.DataFetchingException;
|
||||
String message = ex.getMessage();
|
||||
Map<String, Object> errorMap = GraphqlErrorBuilder.newError().errorType(errorType).message(message).build()
|
||||
.toSpecification();
|
||||
return Mono.just(encode(id, MessageType.ERROR, errorMap));
|
||||
});
|
||||
return outputFlux
|
||||
.map((result) -> {
|
||||
Map<String, Object> dataMap = result.toSpecification();
|
||||
return encode(id, MessageType.NEXT, dataMap);
|
||||
})
|
||||
.concatWith(Mono.fromCallable(() -> encode(id, MessageType.COMPLETE, null)))
|
||||
.onErrorResume((ex) -> {
|
||||
if (ex instanceof SubscriptionExistsException) {
|
||||
CloseStatus status = new CloseStatus(4409, "Subscriber for " + id + " already exists");
|
||||
GraphQlStatus.closeSession(session, status);
|
||||
return Flux.empty();
|
||||
}
|
||||
ErrorType errorType = ErrorType.DataFetchingException;
|
||||
String message = ex.getMessage();
|
||||
Map<String, Object> errorMap = GraphqlErrorBuilder.newError().errorType(errorType).message(message).build()
|
||||
.toSpecification();
|
||||
return Mono.just(encode(id, MessageType.ERROR, errorMap));
|
||||
});
|
||||
}
|
||||
|
||||
// @formatter:on
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> TextMessage encode(@Nullable String id, MessageType messageType, @Nullable Object payload) {
|
||||
Map<String, Object> payloadMap = new HashMap<>(3);
|
||||
@@ -282,8 +303,16 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub
|
||||
|
||||
private enum MessageType {
|
||||
|
||||
CONNECTION_INIT("connection_init"), CONNECTION_ACK("connection_ack"), SUBSCRIBE("subscribe"), NEXT(
|
||||
"next"), ERROR("error"), COMPLETE("complete");
|
||||
// @formatter:off
|
||||
|
||||
CONNECTION_INIT("connection_init"),
|
||||
CONNECTION_ACK("connection_ack"),
|
||||
SUBSCRIBE("subscribe"),
|
||||
NEXT("next"),
|
||||
ERROR("error"),
|
||||
COMPLETE("complete");
|
||||
|
||||
// @formatter:on
|
||||
|
||||
private static final Map<String, MessageType> messageTypes = new HashMap<>(6);
|
||||
|
||||
@@ -312,15 +341,17 @@ public class GraphQlWebSocketHandler extends TextWebSocketHandler implements Sub
|
||||
|
||||
private static class GraphQlStatus {
|
||||
|
||||
// @formatter:off
|
||||
|
||||
private static final CloseStatus INVALID_MESSAGE_STATUS = new CloseStatus(4400, "Invalid message");
|
||||
|
||||
private static final CloseStatus UNAUTHORIZED_STATUS = new CloseStatus(4401, "Unauthorized");
|
||||
|
||||
private static final CloseStatus INIT_TIMEOUT_STATUS = new CloseStatus(4408,
|
||||
"Connection initialisation timeout");
|
||||
private static final CloseStatus INIT_TIMEOUT_STATUS = new CloseStatus(4408, "Connection initialisation timeout");
|
||||
|
||||
private static final CloseStatus TOO_MANY_INIT_REQUESTS_STATUS = new CloseStatus(4429,
|
||||
"Too many initialisation requests");
|
||||
private static final CloseStatus TOO_MANY_INIT_REQUESTS_STATUS = new CloseStatus(4429, "Too many initialisation requests");
|
||||
|
||||
// @formatter:on
|
||||
|
||||
static void closeSession(WebSocketSession session, CloseStatus status) {
|
||||
try {
|
||||
|
||||
@@ -27,28 +27,34 @@ import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
|
||||
import org.springframework.graphql.execution.GraphQlSource;
|
||||
|
||||
// @formatter:off
|
||||
|
||||
/**
|
||||
* Utility methods for GraphQL tests.
|
||||
*/
|
||||
public abstract class GraphQlTestUtils {
|
||||
|
||||
public static GraphQL initGraphQl(String schemaContent, String typeName, String fieldName, DataFetcher<?> fetcher) {
|
||||
|
||||
return initGraphQlSource(schemaContent, typeName, fieldName, fetcher).build().graphQl();
|
||||
return initGraphQlSource(schemaContent, typeName, fieldName, fetcher)
|
||||
.build()
|
||||
.graphQl();
|
||||
}
|
||||
|
||||
public static GraphQL initGraphQl(String schemaContent, String typeName, String fieldName, DataFetcher<?> fetcher,
|
||||
DataFetcherExceptionResolver... resolvers) {
|
||||
|
||||
return initGraphQlSource(schemaContent, typeName, fieldName, fetcher)
|
||||
.exceptionResolvers(Arrays.asList(resolvers)).build().graphQl();
|
||||
.exceptionResolvers(Arrays.asList(resolvers))
|
||||
.build()
|
||||
.graphQl();
|
||||
}
|
||||
|
||||
public static GraphQlSource.Builder initGraphQlSource(String schemaContent, String typeName, String fieldName,
|
||||
DataFetcher<?> fetcher) {
|
||||
|
||||
RuntimeWiring wiring = RuntimeWiring.newRuntimeWiring()
|
||||
.type(typeName, (builder) -> builder.dataFetcher(fieldName, fetcher)).build();
|
||||
.type(typeName, (builder) -> builder.dataFetcher(fieldName, fetcher))
|
||||
.build();
|
||||
|
||||
return GraphQlSource.builder()
|
||||
.schemaResource(new ByteArrayResource(schemaContent.getBytes(StandardCharsets.UTF_8)))
|
||||
|
||||
@@ -35,6 +35,8 @@ import org.springframework.graphql.TestThreadLocalAccessor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
// @formatter:off
|
||||
|
||||
/**
|
||||
* Tests for {@link ContextDataFetcherDecorator}.
|
||||
*/
|
||||
@@ -76,10 +78,11 @@ public class ContextDataFetcherDecoratorTests {
|
||||
void fluxDataFetcherSubscription() throws Exception {
|
||||
GraphQL graphQl = GraphQlTestUtils.initGraphQl(
|
||||
"type Query { greeting: String } type Subscription { greetings: String }", "Subscription", "greetings",
|
||||
(env) -> Mono.delay(Duration.ofMillis(50)).flatMapMany((aLong) -> Flux.deferContextual((context) -> {
|
||||
String name = context.get("name");
|
||||
return Flux.just("Hi", "Bonjour", "Hola").map((s) -> s + " " + name);
|
||||
})));
|
||||
(env) -> Mono.delay(Duration.ofMillis(50))
|
||||
.flatMapMany((aLong) -> Flux.deferContextual((context) -> {
|
||||
String name = context.get("name");
|
||||
return Flux.just("Hi", "Bonjour", "Hola").map((s) -> s + " " + name);
|
||||
})));
|
||||
|
||||
ExecutionInput input = ExecutionInput.newExecutionInput().query("subscription { greetings }").build();
|
||||
ContextManager.setReactorContext(Context.of("name", "007"), input);
|
||||
@@ -87,7 +90,9 @@ public class ContextDataFetcherDecoratorTests {
|
||||
Publisher<String> publisher = graphQl.executeAsync(input).get().getData();
|
||||
|
||||
List<String> actual = Flux.from(publisher).cast(ExecutionResult.class)
|
||||
.map((result) -> ((Map<String, ?>) result.getData()).get("greetings")).cast(String.class).collectList()
|
||||
.map((result) -> ((Map<String, ?>) result.getData()).get("greetings"))
|
||||
.cast(String.class)
|
||||
.collectList()
|
||||
.block();
|
||||
|
||||
assertThat(actual).containsExactly("Hi 007", "Bonjour 007", "Hola 007");
|
||||
@@ -99,7 +104,8 @@ public class ContextDataFetcherDecoratorTests {
|
||||
nameThreadLocal.set("007");
|
||||
TestThreadLocalAccessor<String> accessor = new TestThreadLocalAccessor<>(nameThreadLocal);
|
||||
try {
|
||||
GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting",
|
||||
GraphQL graphQl = GraphQlTestUtils.initGraphQl(
|
||||
"type Query { greeting: String }", "Query", "greeting",
|
||||
(env) -> "Hello " + nameThreadLocal.get());
|
||||
|
||||
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
|
||||
@@ -107,7 +113,8 @@ public class ContextDataFetcherDecoratorTests {
|
||||
ContextManager.setReactorContext(view, input);
|
||||
|
||||
ExecutionResult result = Mono.delay(Duration.ofMillis(10))
|
||||
.flatMap((aLong) -> Mono.fromFuture(graphQl.executeAsync(input))).block();
|
||||
.flatMap((aLong) -> Mono.fromFuture(graphQl.executeAsync(input)))
|
||||
.block();
|
||||
|
||||
Map<String, Object> data = result.getData();
|
||||
assertThat(data).hasSize(1).containsEntry("greeting", "Hello 007");
|
||||
|
||||
@@ -36,6 +36,8 @@ import org.springframework.graphql.TestThreadLocalAccessor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
// @formatter:off
|
||||
|
||||
/**
|
||||
* Tests for {@link ExceptionResolversExceptionHandler}.
|
||||
*/
|
||||
@@ -46,8 +48,11 @@ public class ExceptionResolversExceptionHandlerTests {
|
||||
GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting",
|
||||
(env) -> {
|
||||
throw new IllegalArgumentException("Invalid greeting");
|
||||
}, (ex, env) -> Mono.just(Collections.singletonList(GraphqlErrorBuilder.newError(env)
|
||||
.message("Resolved error: " + ex.getMessage()).errorType(ErrorType.BAD_REQUEST).build())));
|
||||
},
|
||||
(ex, env) -> Mono.just(Collections.singletonList(
|
||||
GraphqlErrorBuilder.newError(env)
|
||||
.message("Resolved error: " + ex.getMessage())
|
||||
.errorType(ErrorType.BAD_REQUEST).build())));
|
||||
|
||||
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
|
||||
ExecutionResult result = graphQl.executeAsync(input).get();
|
||||
@@ -67,9 +72,10 @@ public class ExceptionResolversExceptionHandlerTests {
|
||||
(env) -> {
|
||||
throw new IllegalArgumentException("Invalid greeting");
|
||||
},
|
||||
(ex, env) -> Mono.deferContextual((view) -> Mono.just(Collections.singletonList(GraphqlErrorBuilder
|
||||
.newError(env).message("Resolved error: " + ex.getMessage() + ", name=" + view.get("name"))
|
||||
.errorType(ErrorType.BAD_REQUEST).build()))));
|
||||
(ex, env) -> Mono.deferContextual((view) -> Mono.just(Collections.singletonList(
|
||||
GraphqlErrorBuilder.newError(env)
|
||||
.message("Resolved error: " + ex.getMessage() + ", name=" + view.get("name"))
|
||||
.errorType(ErrorType.BAD_REQUEST).build()))));
|
||||
|
||||
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
|
||||
ContextManager.setReactorContext(Context.of("name", "007"), input);
|
||||
@@ -90,17 +96,19 @@ public class ExceptionResolversExceptionHandlerTests {
|
||||
(env) -> {
|
||||
throw new IllegalArgumentException("Invalid greeting");
|
||||
},
|
||||
(SyncDataFetcherExceptionResolver) (ex,
|
||||
env) -> Collections.singletonList(GraphqlErrorBuilder.newError(env)
|
||||
(SyncDataFetcherExceptionResolver) (ex, env) -> Collections.singletonList(
|
||||
GraphqlErrorBuilder.newError(env)
|
||||
.message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get())
|
||||
.errorType(ErrorType.BAD_REQUEST).build()));
|
||||
.errorType(ErrorType.BAD_REQUEST)
|
||||
.build()));
|
||||
|
||||
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
|
||||
ContextView view = ContextManager.extractThreadLocalValues(accessor);
|
||||
ContextManager.setReactorContext(view, input);
|
||||
|
||||
ExecutionResult result = Mono.delay(Duration.ofMillis(10))
|
||||
.flatMap((aLong) -> Mono.fromFuture(graphQl.executeAsync(input))).block();
|
||||
.flatMap((aLong) -> Mono.fromFuture(graphQl.executeAsync(input)))
|
||||
.block();
|
||||
|
||||
List<GraphQLError> errors = result.getErrors();
|
||||
assertThat(errors.get(0).getMessage()).isEqualTo("Resolved error: Invalid greeting, name=007");
|
||||
@@ -115,7 +123,8 @@ public class ExceptionResolversExceptionHandlerTests {
|
||||
GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting",
|
||||
(env) -> {
|
||||
throw new IllegalArgumentException("Invalid greeting");
|
||||
}, (exception, environment) -> Mono.empty());
|
||||
},
|
||||
(exception, environment) -> Mono.empty());
|
||||
|
||||
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
|
||||
ExecutionResult result = graphQl.executeAsync(input).get();
|
||||
@@ -135,7 +144,8 @@ public class ExceptionResolversExceptionHandlerTests {
|
||||
GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting",
|
||||
(env) -> {
|
||||
throw new IllegalArgumentException("Invalid greeting");
|
||||
}, (ex, env) -> Mono.just(Collections.emptyList()));
|
||||
},
|
||||
(ex, env) -> Mono.just(Collections.emptyList()));
|
||||
|
||||
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
|
||||
ExecutionResult result = graphQl.executeAsync(input).get();
|
||||
|
||||
@@ -28,17 +28,33 @@ import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.graphql.execution.ExecutionGraphQlService;
|
||||
import org.springframework.graphql.execution.GraphQlSource;
|
||||
|
||||
// @formatter:off
|
||||
|
||||
public abstract class BookTestUtils {
|
||||
|
||||
public static final String SUBSCRIPTION_ID = "1";
|
||||
|
||||
public static final String BOOK_QUERY = "{" + "\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\","
|
||||
+ "\"type\":\"subscribe\"," + "\"payload\":{\"query\": \"" + " query TestQuery {"
|
||||
+ " bookById(id: \\\"1\\\"){ " + " id" + " name" + " author" + " }}\"}" + "}";
|
||||
public static final String BOOK_QUERY = "{" +
|
||||
"\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\"," +
|
||||
"\"type\":\"subscribe\"," +
|
||||
"\"payload\":{\"query\": \"" +
|
||||
" query TestQuery {" +
|
||||
" bookById(id: \\\"1\\\"){ " +
|
||||
" id" + " name" +
|
||||
" author" + " }}\"}" +
|
||||
"}";
|
||||
|
||||
public static final String BOOK_SUBSCRIPTION = "{" + "\"id\":\"" + SUBSCRIPTION_ID + "\","
|
||||
+ "\"type\":\"subscribe\"," + "\"payload\":{\"query\": \"" + " subscription TestSubscription {"
|
||||
+ " bookSearch(author: \\\"George\\\") {" + " id" + " name" + " author" + " }}\"}" + "}";
|
||||
public static final String BOOK_SUBSCRIPTION = "{" +
|
||||
"\"id\":\"" + SUBSCRIPTION_ID + "\"," +
|
||||
"\"type\":\"subscribe\"," +
|
||||
"\"payload\":{\"query\": \"" +
|
||||
" subscription TestSubscription {" +
|
||||
" bookSearch(author: \\\"George\\\") {" +
|
||||
" id" +
|
||||
" name" +
|
||||
" author" +
|
||||
" }}\"}" +
|
||||
"}";
|
||||
|
||||
private static final Map<Long, Book> booksMap = new HashMap<>(4);
|
||||
static {
|
||||
@@ -51,21 +67,26 @@ public abstract class BookTestUtils {
|
||||
|
||||
public static WebGraphQlHandler initWebGraphQlHandler(WebInterceptor... interceptors) {
|
||||
return WebGraphQlHandler.builder(new ExecutionGraphQlService(graphQlSource()))
|
||||
.interceptors(Arrays.asList(interceptors)).build();
|
||||
.interceptors(Arrays.asList(interceptors))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static GraphQlSource graphQlSource() {
|
||||
RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring();
|
||||
builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById", (env) -> {
|
||||
Long id = Long.parseLong(env.getArgument("id"));
|
||||
return booksMap.get(id);
|
||||
}));
|
||||
builder.type(TypeRuntimeWiring.newTypeWiring("Subscription").dataFetcher("bookSearch", (env) -> {
|
||||
String author = env.getArgument("author");
|
||||
return Flux.fromIterable(booksMap.values()).filter((book) -> book.getAuthor().contains(author));
|
||||
}));
|
||||
return GraphQlSource.builder().schemaResource(new ClassPathResource("books/schema.graphqls"))
|
||||
.runtimeWiring(builder.build()).build();
|
||||
builder.type(TypeRuntimeWiring.newTypeWiring("Query")
|
||||
.dataFetcher("bookById", (env) -> {
|
||||
Long id = Long.parseLong(env.getArgument("id"));
|
||||
return booksMap.get(id);
|
||||
}));
|
||||
builder.type(TypeRuntimeWiring.newTypeWiring("Subscription")
|
||||
.dataFetcher("bookSearch", (env) -> {
|
||||
String author = env.getArgument("author");
|
||||
return Flux.fromIterable(booksMap.values()).filter((book) -> book.getAuthor().contains(author));
|
||||
}));
|
||||
return GraphQlSource.builder()
|
||||
.schemaResource(new ClassPathResource("books/schema.graphqls"))
|
||||
.runtimeWiring(builder.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,17 +39,20 @@ import org.springframework.http.HttpHeaders;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
// @formatter:off
|
||||
|
||||
/**
|
||||
* Tests for {@link WebGraphQlHandler}, common to both HTTP and WebSocket.
|
||||
*/
|
||||
public class WebGraphQlHandlerTests {
|
||||
|
||||
private static final WebInput webInput = new WebInput(URI.create("http://abc.org"), new HttpHeaders(),
|
||||
Collections.singletonMap("query", "{ greeting }"), "1");
|
||||
private static final WebInput webInput = new WebInput(
|
||||
URI.create("http://abc.org"), new HttpHeaders(), Collections.singletonMap("query", "{ greeting }"), "1");
|
||||
|
||||
@Test
|
||||
void reactorContextPropagation() {
|
||||
GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting",
|
||||
GraphQL graphQl = GraphQlTestUtils.initGraphQl(
|
||||
"type Query { greeting: String }", "Query", "greeting",
|
||||
(env) -> Mono.deferContextual((context) -> {
|
||||
Object name = context.get("name");
|
||||
return Mono.delay(Duration.ofMillis(50)).map((aLong) -> "Hello " + name);
|
||||
@@ -70,7 +73,8 @@ public class WebGraphQlHandlerTests {
|
||||
(env) -> {
|
||||
throw new IllegalArgumentException("Invalid greeting");
|
||||
},
|
||||
(ex, env) -> Mono.deferContextual((view) -> Mono.just(Collections.singletonList(GraphqlErrorBuilder
|
||||
(ex, env) -> Mono.deferContextual((view) -> Mono.just(Collections.singletonList(
|
||||
GraphqlErrorBuilder
|
||||
.newError(env).message("Resolved error: " + ex.getMessage() + ", name=" + view.get("name"))
|
||||
.errorType(ErrorType.BAD_REQUEST).build()))));
|
||||
|
||||
@@ -93,15 +97,16 @@ public class WebGraphQlHandlerTests {
|
||||
nameThreadLocal.set("007");
|
||||
TestThreadLocalAccessor<String> threadLocalAccessor = new TestThreadLocalAccessor<>(nameThreadLocal);
|
||||
try {
|
||||
GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }", "Query", "greeting",
|
||||
GraphQL graphQl = GraphQlTestUtils.initGraphQl(
|
||||
"type Query { greeting: String }", "Query", "greeting",
|
||||
(env) -> "Hello " + nameThreadLocal.get());
|
||||
|
||||
GraphQlService service = new ExecutionGraphQlService(new TestGraphQlSource(graphQl));
|
||||
|
||||
WebGraphQlHandler handler = WebGraphQlHandler.builder(service)
|
||||
.interceptor(
|
||||
(input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap((aLong) -> next.handle(input)))
|
||||
.threadLocalAccessor(threadLocalAccessor).build();
|
||||
.interceptor((input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap((aLong) -> next.handle(input)))
|
||||
.threadLocalAccessor(threadLocalAccessor)
|
||||
.build();
|
||||
|
||||
Map<String, Object> data = handler.handle(webInput).block().getData();
|
||||
|
||||
@@ -122,17 +127,17 @@ public class WebGraphQlHandlerTests {
|
||||
(env) -> {
|
||||
throw new IllegalArgumentException("Invalid greeting");
|
||||
},
|
||||
(SyncDataFetcherExceptionResolver) (ex,
|
||||
env) -> Collections.singletonList(GraphqlErrorBuilder.newError(env)
|
||||
(SyncDataFetcherExceptionResolver) (ex, env) -> Collections.singletonList(
|
||||
GraphqlErrorBuilder.newError(env)
|
||||
.message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get())
|
||||
.errorType(ErrorType.BAD_REQUEST).build()));
|
||||
|
||||
GraphQlService service = new ExecutionGraphQlService(new TestGraphQlSource(graphQl));
|
||||
|
||||
WebGraphQlHandler handler = WebGraphQlHandler.builder(service)
|
||||
.interceptor(
|
||||
(input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap((aLong) -> next.handle(input)))
|
||||
.threadLocalAccessor(threadLocalAccessor).build();
|
||||
.interceptor((input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap((aLong) -> next.handle(input)))
|
||||
.threadLocalAccessor(threadLocalAccessor)
|
||||
.build();
|
||||
|
||||
WebOutput webOutput = handler.handle(webInput).block();
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ import org.springframework.http.HttpHeaders;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
// @formatter:off
|
||||
|
||||
/**
|
||||
* Unit tests for a {@link WebInterceptor} chain.
|
||||
*/
|
||||
@@ -44,7 +46,9 @@ public class WebInterceptorTests {
|
||||
StringBuilder output = new StringBuilder();
|
||||
|
||||
WebGraphQlHandler handler = WebGraphQlHandler.builder((input) -> emptyExecutionResult())
|
||||
.interceptors(Arrays.asList(new OrderInterceptor(1, output), new OrderInterceptor(2, output),
|
||||
.interceptors(Arrays.asList(
|
||||
new OrderInterceptor(1, output),
|
||||
new OrderInterceptor(2, output),
|
||||
new OrderInterceptor(3, output)))
|
||||
.build();
|
||||
|
||||
@@ -55,8 +59,7 @@ public class WebInterceptorTests {
|
||||
@Test
|
||||
void responseHeader() {
|
||||
WebGraphQlHandler handler = WebGraphQlHandler.builder((input) -> emptyExecutionResult())
|
||||
.interceptor((input, next) -> next.handle(input).map(
|
||||
(output) -> output.transform((builder) -> builder.responseHeader("testHeader", "testValue"))))
|
||||
.interceptor((input, next) -> next.handle(input).map((output) -> output.transform((builder) -> builder.responseHeader("testHeader", "testValue"))))
|
||||
.build();
|
||||
|
||||
HttpHeaders headers = handler.handle(webInput).block().getResponseHeaders();
|
||||
@@ -68,13 +71,16 @@ public class WebInterceptorTests {
|
||||
void executionInputCustomization() {
|
||||
AtomicReference<String> actualName = new AtomicReference<>();
|
||||
|
||||
WebGraphQlHandler handler = WebGraphQlHandler.builder((input) -> {
|
||||
actualName.set(input.getOperationName());
|
||||
return emptyExecutionResult();
|
||||
}).interceptor((webInput, next) -> {
|
||||
webInput.configureExecutionInput((input, builder) -> builder.operationName("testOp").build());
|
||||
return next.handle(webInput);
|
||||
}).build();
|
||||
WebGraphQlHandler handler = WebGraphQlHandler.builder(
|
||||
(input) -> {
|
||||
actualName.set(input.getOperationName());
|
||||
return emptyExecutionResult();
|
||||
})
|
||||
.interceptor((webInput, next) -> {
|
||||
webInput.configureExecutionInput((input, builder) -> builder.operationName("testOp").build());
|
||||
return next.handle(webInput);
|
||||
})
|
||||
.build();
|
||||
|
||||
handler.handle(webInput).block();
|
||||
|
||||
@@ -99,10 +105,12 @@ public class WebInterceptorTests {
|
||||
@Override
|
||||
public Mono<WebOutput> intercept(WebInput input, WebGraphQlHandler next) {
|
||||
this.output.append(":pre").append(this.order);
|
||||
return next.handle(input).map((output) -> {
|
||||
this.output.append(":post").append(this.order);
|
||||
return output;
|
||||
}).subscribeOn(Schedulers.boundedElastic());
|
||||
return next.handle(input)
|
||||
.map((output) -> {
|
||||
this.output.append(":post").append(this.order);
|
||||
return output;
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ import org.springframework.web.reactive.socket.WebSocketMessage;
|
||||
import static org.assertj.core.api.Assertions.as;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
// @formatter:off
|
||||
|
||||
/**
|
||||
* Unit tests for {@link GraphQlWebSocketHandler}.
|
||||
*/
|
||||
@@ -53,7 +55,8 @@ public class GraphQlWebSocketHandlerTests {
|
||||
|
||||
@Test
|
||||
void query() {
|
||||
TestWebSocketSession session = handle(Flux.just(toWebSocketMessage("{\"type\":\"connection_init\"}"),
|
||||
TestWebSocketSession session = handle(Flux.just(
|
||||
toWebSocketMessage("{\"type\":\"connection_init\"}"),
|
||||
toWebSocketMessage(BookTestUtils.BOOK_QUERY)));
|
||||
|
||||
StepVerifier.create(session.getOutput())
|
||||
@@ -64,51 +67,61 @@ public class GraphQlWebSocketHandlerTests {
|
||||
.extractingByKey("data", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.extractingByKey("bookById", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.containsEntry("name", "Nineteen Eighty-Four"))
|
||||
.consumeNextWith((message) -> assertMessageType(message, "complete")).verifyComplete();
|
||||
.consumeNextWith((message) -> assertMessageType(message, "complete"))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void subscription() {
|
||||
TestWebSocketSession session = handle(Flux.just(toWebSocketMessage("{\"type\":\"connection_init\"}"),
|
||||
TestWebSocketSession session = handle(Flux.just(
|
||||
toWebSocketMessage("{\"type\":\"connection_init\"}"),
|
||||
toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION)));
|
||||
|
||||
BiConsumer<WebSocketMessage, String> bookPayloadAssertion = (message, bookId) -> assertThat(decode(message))
|
||||
.hasSize(3).containsEntry("id", BookTestUtils.SUBSCRIPTION_ID).containsEntry("type", "next")
|
||||
.extractingByKey("payload", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.extractingByKey("data", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.extractingByKey("bookSearch", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.containsEntry("id", bookId);
|
||||
BiConsumer<WebSocketMessage, String> bookPayloadAssertion = (message, bookId) ->
|
||||
assertThat(decode(message))
|
||||
.hasSize(3).containsEntry("id", BookTestUtils.SUBSCRIPTION_ID).containsEntry("type", "next")
|
||||
.extractingByKey("payload", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.extractingByKey("data", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.extractingByKey("bookSearch", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.containsEntry("id", bookId);
|
||||
|
||||
StepVerifier.create(session.getOutput())
|
||||
.consumeNextWith((message) -> assertMessageType(message, "connection_ack"))
|
||||
.consumeNextWith((message) -> bookPayloadAssertion.accept(message, "1"))
|
||||
.consumeNextWith((message) -> bookPayloadAssertion.accept(message, "5"))
|
||||
.consumeNextWith((message) -> assertMessageType(message, "complete")).verifyComplete();
|
||||
.consumeNextWith((message) -> assertMessageType(message, "complete"))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unauthorizedWithoutMessageType() {
|
||||
TestWebSocketSession session = handle(Flux.just(toWebSocketMessage("{\"type\":\"connection_init\"}"),
|
||||
TestWebSocketSession session = handle(Flux.just(
|
||||
toWebSocketMessage("{\"type\":\"connection_init\"}"),
|
||||
toWebSocketMessage("{\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\"}")));
|
||||
|
||||
StepVerifier.create(session.getOutput())
|
||||
.consumeNextWith((message) -> assertMessageType(message, "connection_ack")).verifyComplete();
|
||||
.consumeNextWith((message) -> assertMessageType(message, "connection_ack"))
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(session.closeStatus()).expectNext(new CloseStatus(4400, "Invalid message"))
|
||||
StepVerifier.create(session.closeStatus())
|
||||
.expectNext(new CloseStatus(4400, "Invalid message"))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidMessageWithoutId() {
|
||||
Flux<WebSocketMessage> input = Flux.just(toWebSocketMessage("{\"type\":\"connection_init\"}"),
|
||||
Flux<WebSocketMessage> input = Flux.just(
|
||||
toWebSocketMessage("{\"type\":\"connection_init\"}"),
|
||||
toWebSocketMessage("{\"type\":\"subscribe\"}")); // No message id
|
||||
|
||||
TestWebSocketSession session = handle(input);
|
||||
|
||||
StepVerifier.create(session.getOutput())
|
||||
.consumeNextWith((message) -> assertMessageType(message, "connection_ack")).verifyComplete();
|
||||
.consumeNextWith((message) -> assertMessageType(message, "connection_ack"))
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(session.closeStatus()).expectNext(new CloseStatus(4400, "Invalid message"))
|
||||
StepVerifier.create(session.closeStatus())
|
||||
.expectNext(new CloseStatus(4400, "Invalid message"))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@@ -122,43 +135,47 @@ public class GraphQlWebSocketHandlerTests {
|
||||
|
||||
@Test
|
||||
void tooManyConnectionInitRequests() {
|
||||
TestWebSocketSession session = handle(Flux.just(toWebSocketMessage("{\"type\":\"connection_init\"}"),
|
||||
TestWebSocketSession session = handle(Flux.just(
|
||||
toWebSocketMessage("{\"type\":\"connection_init\"}"),
|
||||
toWebSocketMessage("{\"type\":\"connection_init\"}")));
|
||||
|
||||
StepVerifier.create(session.getOutput())
|
||||
.consumeNextWith((message) -> assertMessageType(message, "connection_ack")).verifyComplete();
|
||||
.consumeNextWith((message) -> assertMessageType(message, "connection_ack"))
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(session.closeStatus()).expectNext(new CloseStatus(4429, "Too many initialisation requests"))
|
||||
StepVerifier.create(session.closeStatus())
|
||||
.expectNext(new CloseStatus(4429, "Too many initialisation requests"))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void connectionInitTimeout() {
|
||||
GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler(BookTestUtils.initWebGraphQlHandler(),
|
||||
ServerCodecConfigurer.create(), Duration.ofMillis(50));
|
||||
GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler(
|
||||
BookTestUtils.initWebGraphQlHandler(), ServerCodecConfigurer.create(), Duration.ofMillis(50));
|
||||
|
||||
TestWebSocketSession session = new TestWebSocketSession(Flux.empty());
|
||||
handler.handle(session).block();
|
||||
|
||||
StepVerifier.create(session.closeStatus())
|
||||
.expectNext(new CloseStatus(4408, "Connection initialisation timeout")).verifyComplete();
|
||||
.expectNext(new CloseStatus(4408, "Connection initialisation timeout"))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void subscriptionExists() {
|
||||
TestWebSocketSession session = handle(
|
||||
Flux.just(toWebSocketMessage("{\"type\":\"connection_init\"}"),
|
||||
toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION),
|
||||
toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION)),
|
||||
new ConsumeOneAndNeverCompleteInterceptor());
|
||||
Flux<WebSocketMessage> messageFlux = Flux.just(
|
||||
toWebSocketMessage("{\"type\":\"connection_init\"}"),
|
||||
toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION),
|
||||
toWebSocketMessage(BookTestUtils.BOOK_SUBSCRIPTION));
|
||||
|
||||
TestWebSocketSession session = handle(messageFlux, new ConsumeOneAndNeverCompleteInterceptor());
|
||||
|
||||
// Collect messages until session closed
|
||||
List<Map<String, Object>> messages = new ArrayList<>();
|
||||
session.getOutput().subscribe((message) -> messages.add(decode(message)));
|
||||
|
||||
StepVerifier.create(session.closeStatus())
|
||||
.expectNext(
|
||||
new CloseStatus(4409, "Subscriber for " + BookTestUtils.SUBSCRIPTION_ID + " already exists"))
|
||||
.expectNext(new CloseStatus(4409, "Subscriber for " + BookTestUtils.SUBSCRIPTION_ID + " already exists"))
|
||||
.verifyComplete();
|
||||
|
||||
assertThat(messages.size()).isEqualTo(2);
|
||||
@@ -188,8 +205,10 @@ public class GraphQlWebSocketHandlerTests {
|
||||
}
|
||||
|
||||
private TestWebSocketSession handle(Flux<WebSocketMessage> input, WebInterceptor... interceptors) {
|
||||
GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler(BookTestUtils.initWebGraphQlHandler(interceptors),
|
||||
ServerCodecConfigurer.create(), Duration.ofSeconds(60));
|
||||
GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler(
|
||||
BookTestUtils.initWebGraphQlHandler(interceptors),
|
||||
ServerCodecConfigurer.create(),
|
||||
Duration.ofSeconds(60));
|
||||
|
||||
TestWebSocketSession session = new TestWebSocketSession(input);
|
||||
handler.handle(session).block();
|
||||
|
||||
@@ -44,6 +44,8 @@ import org.springframework.web.socket.WebSocketMessage;
|
||||
import static org.assertj.core.api.Assertions.as;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
// @formatter:off
|
||||
|
||||
/**
|
||||
* Unit tests for {@link GraphQlWebSocketHandler}.
|
||||
*/
|
||||
@@ -57,7 +59,8 @@ public class GraphQlWebSocketHandlerTests {
|
||||
|
||||
@Test
|
||||
void query() throws Exception {
|
||||
handle(this.handler, new TextMessage("{\"type\":\"connection_init\"}"),
|
||||
handle(this.handler,
|
||||
new TextMessage("{\"type\":\"connection_init\"}"),
|
||||
new TextMessage(BookTestUtils.BOOK_QUERY));
|
||||
|
||||
StepVerifier.create(this.session.getOutput())
|
||||
@@ -68,9 +71,8 @@ public class GraphQlWebSocketHandlerTests {
|
||||
.extractingByKey("data", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.extractingByKey("bookById", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.containsEntry("name", "Nineteen Eighty-Four"))
|
||||
.consumeNextWith((message) -> assertMessageType(message, "complete")).then(this.session::close) // Complete
|
||||
// output
|
||||
// Flux
|
||||
.consumeNextWith((message) -> assertMessageType(message, "complete"))
|
||||
.then(this.session::close) // Complete output Flux
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@@ -79,41 +81,46 @@ public class GraphQlWebSocketHandlerTests {
|
||||
handle(this.handler, new TextMessage("{\"type\":\"connection_init\"}"),
|
||||
new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION));
|
||||
|
||||
BiConsumer<WebSocketMessage<?>, String> bookPayloadAssertion = (message, bookId) -> assertThat(decode(message))
|
||||
.hasSize(3).containsEntry("id", BookTestUtils.SUBSCRIPTION_ID).containsEntry("type", "next")
|
||||
.extractingByKey("payload", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.extractingByKey("data", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.extractingByKey("bookSearch", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.containsEntry("id", bookId);
|
||||
BiConsumer<WebSocketMessage<?>, String> bookPayloadAssertion = (message, bookId) ->
|
||||
assertThat(decode(message))
|
||||
.hasSize(3).containsEntry("id", BookTestUtils.SUBSCRIPTION_ID).containsEntry("type", "next")
|
||||
.extractingByKey("payload", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.extractingByKey("data", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.extractingByKey("bookSearch", as(InstanceOfAssertFactories.map(String.class, Object.class)))
|
||||
.containsEntry("id", bookId);
|
||||
|
||||
StepVerifier.create(this.session.getOutput())
|
||||
.consumeNextWith((message) -> assertMessageType(message, "connection_ack"))
|
||||
.consumeNextWith((message) -> bookPayloadAssertion.accept(message, "1"))
|
||||
.consumeNextWith((message) -> bookPayloadAssertion.accept(message, "5"))
|
||||
.consumeNextWith((message) -> assertMessageType(message, "complete")).then(this.session::close)
|
||||
// Complete output Flux
|
||||
.consumeNextWith((message) -> assertMessageType(message, "complete"))
|
||||
.then(this.session::close)// Complete output Flux
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unauthorizedWithoutMessageType() throws Exception {
|
||||
handle(this.handler, new TextMessage("{\"type\":\"connection_init\"}"),
|
||||
handle(this.handler,
|
||||
new TextMessage("{\"type\":\"connection_init\"}"),
|
||||
new TextMessage("{\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\"}"));
|
||||
// No message type
|
||||
|
||||
StepVerifier.create(this.session.getOutput())
|
||||
.consumeNextWith((message) -> assertMessageType(message, "connection_ack")).verifyComplete();
|
||||
.consumeNextWith((message) -> assertMessageType(message, "connection_ack"))
|
||||
.verifyComplete();
|
||||
|
||||
assertThat(this.session.getCloseStatus()).isEqualTo(new CloseStatus(4400, "Invalid message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidMessageWithoutId() throws Exception {
|
||||
handle(this.handler, new TextMessage("{\"type\":\"connection_init\"}"),
|
||||
handle(this.handler,
|
||||
new TextMessage("{\"type\":\"connection_init\"}"),
|
||||
new TextMessage("{\"type\":\"subscribe\"}")); // No message id
|
||||
|
||||
StepVerifier.create(this.session.getOutput())
|
||||
.consumeNextWith((message) -> assertMessageType(message, "connection_ack")).verifyComplete();
|
||||
.consumeNextWith((message) -> assertMessageType(message, "connection_ack"))
|
||||
.verifyComplete();
|
||||
|
||||
assertThat(this.session.getCloseStatus()).isEqualTo(new CloseStatus(4400, "Invalid message"));
|
||||
}
|
||||
@@ -128,30 +135,34 @@ public class GraphQlWebSocketHandlerTests {
|
||||
|
||||
@Test
|
||||
void tooManyConnectionInitRequests() throws Exception {
|
||||
handle(this.handler, new TextMessage("{\"type\":\"connection_init\"}"),
|
||||
handle(this.handler,
|
||||
new TextMessage("{\"type\":\"connection_init\"}"),
|
||||
new TextMessage("{\"type\":\"connection_init\"}"));
|
||||
|
||||
StepVerifier.create(this.session.getOutput())
|
||||
.consumeNextWith((message) -> assertMessageType(message, "connection_ack")).verifyComplete();
|
||||
.consumeNextWith((message) -> assertMessageType(message, "connection_ack"))
|
||||
.verifyComplete();
|
||||
|
||||
assertThat(this.session.getCloseStatus()).isEqualTo(new CloseStatus(4429, "Too many initialisation requests"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void connectionInitTimeout() {
|
||||
GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler(BookTestUtils.initWebGraphQlHandler(), converter,
|
||||
Duration.ofMillis(50));
|
||||
GraphQlWebSocketHandler handler = new GraphQlWebSocketHandler(
|
||||
BookTestUtils.initWebGraphQlHandler(), converter, Duration.ofMillis(50));
|
||||
|
||||
handler.afterConnectionEstablished(this.session);
|
||||
|
||||
StepVerifier.create(this.session.closeStatus())
|
||||
.expectNext(new CloseStatus(4408, "Connection initialisation timeout")).verifyComplete();
|
||||
.expectNext(new CloseStatus(4408, "Connection initialisation timeout"))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void subscriptionExists() throws Exception {
|
||||
handle(initWebSocketHandler(new ConsumeOneAndNeverCompleteInterceptor()),
|
||||
new TextMessage("{\"type\":\"connection_init\"}"), new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION),
|
||||
new TextMessage("{\"type\":\"connection_init\"}"),
|
||||
new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION),
|
||||
new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION));
|
||||
|
||||
// Collect messages until session closed
|
||||
@@ -159,8 +170,7 @@ public class GraphQlWebSocketHandlerTests {
|
||||
this.session.getOutput().subscribe((message) -> messages.add(decode(message)));
|
||||
|
||||
StepVerifier.create(this.session.closeStatus())
|
||||
.expectNext(
|
||||
new CloseStatus(4409, "Subscriber for " + BookTestUtils.SUBSCRIPTION_ID + " already exists"))
|
||||
.expectNext(new CloseStatus(4409, "Subscriber for " + BookTestUtils.SUBSCRIPTION_ID + " already exists"))
|
||||
.verifyComplete();
|
||||
|
||||
assertThat(messages.size()).isEqualTo(2);
|
||||
@@ -172,7 +182,8 @@ public class GraphQlWebSocketHandlerTests {
|
||||
void clientCompletion() throws Exception {
|
||||
GraphQlWebSocketHandler handler = initWebSocketHandler(new ConsumeOneAndNeverCompleteInterceptor());
|
||||
|
||||
handle(handler, new TextMessage("{\"type\":\"connection_init\"}"),
|
||||
handle(handler,
|
||||
new TextMessage("{\"type\":\"connection_init\"}"),
|
||||
new TextMessage(BookTestUtils.BOOK_SUBSCRIPTION));
|
||||
|
||||
String completeMessage = "{\"id\":\"" + BookTestUtils.SUBSCRIPTION_ID + "\",\"type\":\"complete\"}";
|
||||
@@ -192,7 +203,8 @@ public class GraphQlWebSocketHandlerTests {
|
||||
.as("Second subscription with same id is possible only if the first was properly removed")
|
||||
.then(() -> messageSender.accept(BookTestUtils.BOOK_SUBSCRIPTION))
|
||||
.consumeNextWith((message) -> assertMessageType(message, "next"))
|
||||
.then(() -> messageSender.accept(completeMessage)).verifyTimeout(Duration.ofMillis(500));
|
||||
.then(() -> messageSender.accept(completeMessage))
|
||||
.verifyTimeout(Duration.ofMillis(500));
|
||||
}
|
||||
|
||||
private void handle(GraphQlWebSocketHandler handler, TextMessage... textMessages) throws Exception {
|
||||
@@ -204,8 +216,8 @@ public class GraphQlWebSocketHandlerTests {
|
||||
|
||||
private GraphQlWebSocketHandler initWebSocketHandler(WebInterceptor... interceptors) {
|
||||
try {
|
||||
return new GraphQlWebSocketHandler(BookTestUtils.initWebGraphQlHandler(interceptors), converter,
|
||||
Duration.ofSeconds(60));
|
||||
return new GraphQlWebSocketHandler(
|
||||
BookTestUtils.initWebGraphQlHandler(interceptors), converter, Duration.ofSeconds(60));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
|
||||
Reference in New Issue
Block a user