diff --git a/spring-graphql-test/build.gradle b/spring-graphql-test/build.gradle index 704751e4..9037dbbf 100644 --- a/spring-graphql-test/build.gradle +++ b/spring-graphql-test/build.gradle @@ -12,7 +12,10 @@ dependencies { compileOnly 'org.springframework:spring-webflux' compileOnly 'org.springframework:spring-webmvc' compileOnly 'org.springframework:spring-websocket' + compileOnly 'org.springframework:spring-messaging' compileOnly 'javax.servlet:javax.servlet-api' + compileOnly 'io.rsocket:rsocket-core' + compileOnly 'io.rsocket:rsocket-transport-netty' compileOnly 'org.skyscreamer:jsonassert' compileOnly 'com.google.code.findbugs:jsr305' compileOnly 'org.jetbrains.kotlinx:kotlinx-coroutines-core' @@ -22,10 +25,12 @@ dependencies { testImplementation 'org.assertj:assertj-core' testImplementation 'org.mockito:mockito-core' testImplementation 'org.skyscreamer:jsonassert' + testImplementation 'org.springframework:spring-messaging' testImplementation 'org.springframework:spring-webflux' testImplementation 'org.springframework:spring-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'io.projectreactor.netty:reactor-netty' + testImplementation 'io.rsocket:rsocket-transport-local' testImplementation 'com.squareup.okhttp3:mockwebserver:3.14.9' testImplementation 'com.fasterxml.jackson.core:jackson-databind' diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java index 0990752a..df6fcad7 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java @@ -24,9 +24,14 @@ import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.spi.json.JacksonJsonProvider; import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; import com.jayway.jsonpath.spi.mapper.MappingProvider; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import org.springframework.graphql.GraphQlRequest; +import org.springframework.graphql.GraphQlResponse; import org.springframework.graphql.ResponseError; import org.springframework.graphql.client.AbstractGraphQlClientBuilder; +import org.springframework.graphql.client.GraphQlClient; import org.springframework.graphql.client.GraphQlTransport; import org.springframework.graphql.support.CachingDocumentSource; import org.springframework.graphql.support.DocumentSource; @@ -131,6 +136,36 @@ public abstract class AbstractGraphQlTesterBuilder execute(GraphQlRequest request) { + return client + .document(request.getDocument()) + .operationName(request.getOperationName()) + .variables(request.getVariables()) + .execute() + .cast(GraphQlResponse.class); + } + + @Override + public Flux executeSubscription(GraphQlRequest request) { + return client + .document(request.getDocument()) + .operationName(request.getOperationName()) + .variables(request.getVariables()) + .executeSubscription() + .cast(GraphQlResponse.class); + } + }; + } + private static class Jackson2Configurer { diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlServiceTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlServiceTester.java index 82491824..1a3f3c87 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlServiceTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlServiceTester.java @@ -37,7 +37,7 @@ final class DefaultGraphQlServiceTester extends AbstractDelegatingGraphQlTester private final Consumer> builderInitializer; - DefaultGraphQlServiceTester(GraphQlTester tester, GraphQlServiceGraphQlTransport transport, + private DefaultGraphQlServiceTester(GraphQlTester tester, GraphQlServiceGraphQlTransport transport, Consumer> builderInitializer) { super(tester); diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java index ccb31e19..65654309 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java @@ -41,7 +41,7 @@ final class DefaultHttpGraphQlTester extends AbstractDelegatingGraphQlTester imp private final Consumer> builderInitializer; - DefaultHttpGraphQlTester(GraphQlTester graphQlTester, WebTestClient webTestClient, + private DefaultHttpGraphQlTester(GraphQlTester graphQlTester, WebTestClient webTestClient, Consumer> builderInitializer) { super(graphQlTester); diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultRSocketGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultRSocketGraphQlTester.java new file mode 100644 index 00000000..19c9dc97 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultRSocketGraphQlTester.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.graphql.test.tester; + +import java.net.URI; +import java.util.List; +import java.util.function.Consumer; + +import io.rsocket.transport.ClientTransport; + +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.graphql.client.RSocketGraphQlClient; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketStrategies; +import org.springframework.util.MimeType; + + +/** + * Default {@link RSocketGraphQlTester} that builds and uses an + * {@link RSocketGraphQlClient} for request execution. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +public class DefaultRSocketGraphQlTester extends AbstractDelegatingGraphQlTester implements RSocketGraphQlTester { + + private final RSocketGraphQlClient rsocketGraphQlClient; + + private final Consumer> builderInitializer; + + + DefaultRSocketGraphQlTester( + GraphQlTester delegate, RSocketGraphQlClient rsocketGraphQlClient, + Consumer> builderInitializer) { + + super(delegate); + this.rsocketGraphQlClient = rsocketGraphQlClient; + this.builderInitializer = builderInitializer; + } + + + @Override + public RSocketGraphQlTester.Builder mutate() { + Builder builder = new Builder(this.rsocketGraphQlClient); + this.builderInitializer.accept(builder); + return builder; + } + + + /** + * Default implementation of {@link GraphQlTester.Builder}. + */ + static final class Builder extends AbstractGraphQlTesterBuilder implements RSocketGraphQlTester.Builder { + + private final RSocketGraphQlClient.Builder rsocketGraphQlClientBuilder; + + /** + * Constructor to start via {@link RSocketGraphQlTester#builder()}. + */ + Builder() { + this.rsocketGraphQlClientBuilder = RSocketGraphQlClient.builder(); + } + + /** + * Constructor to start via {@link RSocketGraphQlTester#builder(RSocketRequester.Builder)}. + */ + Builder(RSocketRequester.Builder requesterBuilder) { + this.rsocketGraphQlClientBuilder = RSocketGraphQlClient.builder(requesterBuilder); + } + + /** + * Constructor to mutate. + * @param rsocketGraphQlClient the underlying client with the current state + */ + public Builder(RSocketGraphQlClient rsocketGraphQlClient) { + this.rsocketGraphQlClientBuilder = rsocketGraphQlClient.mutate(); + } + + @Override + public Builder tcp(String host, int port) { + this.rsocketGraphQlClientBuilder.tcp(host, port); + return this; + } + + @Override + public Builder webSocket(URI uri) { + this.rsocketGraphQlClientBuilder.webSocket(uri); + return this; + } + + @Override + public Builder clientTransport(ClientTransport clientTransport) { + this.rsocketGraphQlClientBuilder.clientTransport(clientTransport); + return this; + } + + @Override + public Builder dataMimeType(MimeType dataMimeType) { + this.rsocketGraphQlClientBuilder.dataMimeType(dataMimeType); + return this; + } + + @Override + public Builder route(String route) { + this.rsocketGraphQlClientBuilder.route(route); + return this; + } + + @Override + public Builder rsocketRequester(Consumer requesterConsumer) { + this.rsocketGraphQlClientBuilder.rsocketRequester(requesterConsumer); + return this; + } + + @Override + public RSocketGraphQlTester build() { + registerJsonPathMappingProvider(); + RSocketGraphQlClient rsocketGraphQlClient = this.rsocketGraphQlClientBuilder.build(); + GraphQlTester graphQlTester = super.buildGraphQlTester(asTransport(rsocketGraphQlClient)); + return new DefaultRSocketGraphQlTester(graphQlTester, rsocketGraphQlClient, getBuilderInitializer()); + } + + private void registerJsonPathMappingProvider() { + this.rsocketGraphQlClientBuilder.rsocketRequester(builder -> + builder.rsocketStrategies(strategiesBuilder -> + configureJsonPathConfig(config -> { + RSocketStrategies strategies = strategiesBuilder.build(); + List> encoders = strategies.encoders(); + List> decoders = strategies.decoders(); + return config.mappingProvider(new EncoderDecoderMappingProvider(encoders, decoders)); + }))); + } + } + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java index 66a6b99d..d368b061 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java @@ -42,7 +42,7 @@ final class DefaultWebGraphQlTester extends AbstractDelegatingGraphQlTester impl private final Consumer> builderInitializer; - DefaultWebGraphQlTester(GraphQlTester tester, WebGraphQlHandlerGraphQlTransport transport, + private DefaultWebGraphQlTester(GraphQlTester tester, WebGraphQlHandlerGraphQlTransport transport, Consumer> builderInitializer) { super(tester); diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java index cc70b823..75535bbb 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java @@ -20,13 +20,8 @@ package org.springframework.graphql.test.tester; import java.net.URI; import java.util.function.Consumer; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.graphql.GraphQlRequest; -import org.springframework.graphql.GraphQlResponse; -import org.springframework.graphql.client.GraphQlClient; -import org.springframework.graphql.client.GraphQlTransport; import org.springframework.graphql.client.WebSocketGraphQlClient; import org.springframework.http.HttpHeaders; import org.springframework.http.codec.CodecConfigurer; @@ -47,7 +42,7 @@ final class DefaultWebSocketGraphQlTester extends AbstractDelegatingGraphQlTeste private final Consumer> builderInitializer; - DefaultWebSocketGraphQlTester( + private DefaultWebSocketGraphQlTester( GraphQlTester graphQlTester, WebSocketGraphQlClient webSocketGraphQlClient, Consumer> builderInitializer) { @@ -154,36 +149,6 @@ final class DefaultWebSocketGraphQlTester extends AbstractDelegatingGraphQlTeste }); }); } - - /** - * GraphQlTransport implementations are private, but we can create the - * GraphQlClient for it and adapt it. - */ - private static GraphQlTransport asTransport(GraphQlClient client) { - return new GraphQlTransport() { - - @Override - public Mono execute(GraphQlRequest request) { - return client - .document(request.getDocument()) - .operationName(request.getOperationName()) - .variables(request.getVariables()) - .execute() - .cast(GraphQlResponse.class); - } - - @Override - public Flux executeSubscription(GraphQlRequest request) { - return client - .document(request.getDocument()) - .operationName(request.getOperationName()) - .variables(request.getVariables()) - .executeSubscription() - .cast(GraphQlResponse.class); - } - }; - } - } } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/EncoderDecoderMappingProvider.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/EncoderDecoderMappingProvider.java index 280cd27a..7458abc1 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/EncoderDecoderMappingProvider.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/EncoderDecoderMappingProvider.java @@ -18,7 +18,9 @@ package org.springframework.graphql.test.tester; import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.stream.Stream; import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.TypeRef; @@ -56,31 +58,55 @@ final class EncoderDecoderMappingProvider implements MappingProvider { /** - * Create an instance by finding the first JSON {@link Encoder} and - * {@link Decoder} in the given {@link CodecConfigurer}. - * @throws IllegalArgumentException if there is no JSON encoder or decoder. + * Create an instance with a {@link CodecConfigurer}. */ public EncoderDecoderMappingProvider(CodecConfigurer configurer) { this.encoder = findJsonEncoder(configurer); this.decoder = findJsonDecoder(configurer); } - private static Decoder findJsonDecoder(CodecConfigurer configurer) { - return configurer.getReaders().stream() - .filter((reader) -> reader.canRead(MAP_TYPE, MediaType.APPLICATION_JSON)) - .map((reader) -> ((DecoderHttpMessageReader) reader).getDecoder()) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("No JSON Decoder")); + /** + * Create an instance with a List of encoders and decoders> + */ + public EncoderDecoderMappingProvider(List> encoders, List> decoders) { + this.encoder = findJsonEncoder(encoders); + this.decoder = findJsonDecoder(decoders); } private static Encoder findJsonEncoder(CodecConfigurer configurer) { - return configurer.getWriters().stream() - .filter((writer) -> writer.canWrite(MAP_TYPE, MediaType.APPLICATION_JSON)) - .map((writer) -> ((EncoderHttpMessageWriter) writer).getEncoder()) + return findJsonEncoder(configurer.getWriters().stream() + .filter(writer -> writer instanceof EncoderHttpMessageWriter) + .map(writer -> ((EncoderHttpMessageWriter) writer).getEncoder())); + } + + private static Decoder findJsonDecoder(CodecConfigurer configurer) { + return findJsonDecoder(configurer.getReaders().stream() + .filter(reader -> reader instanceof DecoderHttpMessageReader) + .map(reader -> ((DecoderHttpMessageReader) reader).getDecoder())); + } + + private static Encoder findJsonEncoder(List> encoders) { + return findJsonEncoder(encoders.stream()); + } + + private static Decoder findJsonDecoder(List> decoders) { + return findJsonDecoder(decoders.stream()); + } + + private static Encoder findJsonEncoder(Stream> stream) { + return stream + .filter(encoder -> encoder.canEncode(MAP_TYPE, MediaType.APPLICATION_JSON)) .findFirst() .orElseThrow(() -> new IllegalArgumentException("No JSON Encoder")); } + private static Decoder findJsonDecoder(Stream> decoderStream) { + return decoderStream + .filter(decoder -> decoder.canDecode(MAP_TYPE, MediaType.APPLICATION_JSON)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No JSON Decoder")); + } + @Nullable @Override diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GenericGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GenericGraphQlTester.java index fe70e0ae..5e30e60f 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GenericGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GenericGraphQlTester.java @@ -35,7 +35,7 @@ final class GenericGraphQlTester extends AbstractDelegatingGraphQlTester { private final Consumer> builderInitializer; - GenericGraphQlTester( + private GenericGraphQlTester( GraphQlTester delegate, GraphQlTransport transport, Consumer> builderInitializer) { diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/RSocketGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/RSocketGraphQlTester.java new file mode 100644 index 00000000..48b7f229 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/RSocketGraphQlTester.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.graphql.test.tester; + + +import java.net.URI; +import java.util.function.Consumer; + +import io.rsocket.transport.ClientTransport; + +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.util.MimeType; + +/** + * GraphQL over RSocket tester that uses {@link RSocketRequester}. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +public interface RSocketGraphQlTester extends GraphQlTester { + + + @Override + RSocketGraphQlTester.Builder mutate(); + + + /** + * Start with a new {@link RSocketRequester.Builder} customized for GraphQL, + * setting the {@code dataMimeType} to {@code "application/graphql+json"} + * and adding JSON codecs. + */ + static RSocketGraphQlTester.Builder builder() { + return new DefaultRSocketGraphQlTester.Builder(); + } + + /** + * Start with a given {@link #builder()}. + */ + static RSocketGraphQlTester.Builder builder(RSocketRequester.Builder requesterBuilder) { + return new DefaultRSocketGraphQlTester.Builder(requesterBuilder); + } + + + /** + * Builder for a GraphQL over RSocket tester. + */ + interface Builder> extends GraphQlTester.Builder { + + /** + * Select TCP as the underlying network protocol. + * @param host the remote host to connect to + * @param port the remote port to connect to + * @return the same builder instance + */ + B tcp(String host, int port); + + /** + * Select WebSocket as the underlying network protocol. + * @param uri the URL for the WebSocket handshake + * @return the same builder instance + */ + B webSocket(URI uri); + + /** + * Use a given {@link ClientTransport} to communicate with the remote server. + * @param clientTransport the transport to use + * @return the same builder instance + */ + B clientTransport(ClientTransport clientTransport); + + /** + * Customize the format of data payloads for the connection. + *

By default, this is set to {@code "application/graphql+json"} but + * it can be changed to {@code "application/json"} if necessary. + * @param dataMimeType the mime type to use + * @return the same builder instance + */ + B dataMimeType(MimeType dataMimeType); + + /** + * Customize the route to specify in the metadata of each request so the + * server can route it to the handler for GraphQL requests. + * @param route the route + * @return the same builder instance + */ + B route(String route); + + /** + * Customize the underlying {@code RSocketRequester} to use. + *

Note that some properties of {@code RSocketRequester.Builder} like the + * data MimeType, and the underlying RSocket transport can be customized + * through this builder. + * @see #dataMimeType(MimeType) + * @see #tcp(String, int) + * @see #webSocket(URI) + * @see #clientTransport(ClientTransport) + * @return the same builder instance + */ + B rsocketRequester(Consumer requester); + + /** + * Build the {@code RSocketGraphQlTester} instance. + */ + @Override + RSocketGraphQlTester build(); + + } + +} diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/RSocketGraphQlTesterBuilderTests.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/RSocketGraphQlTesterBuilderTests.java new file mode 100644 index 00000000..5cddf603 --- /dev/null +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/RSocketGraphQlTesterBuilderTests.java @@ -0,0 +1,245 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.graphql.test.tester; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import io.rsocket.Closeable; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketServer; +import io.rsocket.transport.local.LocalClientTransport; +import io.rsocket.transport.local.LocalServerTransport; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.DecodingException; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.graphql.ExecutionGraphQlResponse; +import org.springframework.graphql.ExecutionGraphQlService; +import org.springframework.graphql.GraphQlRequest; +import org.springframework.graphql.server.GraphQlRSocketHandler; +import org.springframework.graphql.support.DefaultExecutionGraphQlResponse; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.lang.Nullable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.rsocket.RSocketStrategies; +import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; +import org.springframework.stereotype.Controller; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * + * @author Rossen Stoyanchev + */ +public class RSocketGraphQlTesterBuilderTests { + + private static final String DOCUMENT = "{ Query }"; + + + private final BuilderSetup builderSetup = new BuilderSetup(); + + + @AfterEach + void tearDown() { + this.builderSetup.shutDown(); + } + + + @Test + void mutate() { + + // Original + RSocketGraphQlTester.Builder builder = this.builderSetup.initBuilder(); + RSocketGraphQlTester tester = builder.build(); + tester.document(DOCUMENT).executeAndVerify(); + + GraphQlRequest request = this.builderSetup.getGraphQlRequest(); + assertThat(request).isNotNull(); + + // Mutate: still works (carries over original default) + tester = tester.mutate().build(); + tester.document(DOCUMENT).executeAndVerify(); + + request = this.builderSetup.getGraphQlRequest(); + assertThat(request).isNotNull(); + } + + @Test + void rsocketStrategiesRegistersJsonPathMappingProvider() { + + TestJackson2JsonDecoder testDecoder = new TestJackson2JsonDecoder(); + + RSocketGraphQlTester.Builder builder = this.builderSetup.initBuilder() + .rsocketRequester(requesterBuilder -> { + RSocketStrategies strategies = RSocketStrategies.builder() + .encoder(new Jackson2JsonEncoder()) + .decoder(testDecoder) + .build(); + requesterBuilder.rsocketStrategies(strategies); + }); + + String document = "{me {name}}"; + MovieCharacter character = MovieCharacter.create("Luke Skywalker"); + this.builderSetup.setMockResponse(document, + ExecutionResultImpl.newExecutionResult() + .data(Collections.singletonMap("me", character)) + .build()); + + RSocketGraphQlTester client = builder.build(); + GraphQlTester.Response response = client.document(document).execute(); + + testDecoder.resetLastValue(); + assertThat(testDecoder.getLastValue()).isNull(); + + assertThat(response).isNotNull(); + response.path("me").entity(MovieCharacter.class).isEqualTo(character); + response.path("me").matchesJson("{name:\"Luke Skywalker\"}"); + assertThat(testDecoder.getLastValue()).isEqualTo(character); + } + + + private static class BuilderSetup { + + private GraphQlRequest graphQlRequest; + + private final Map responses = new HashMap<>(); + + @Nullable + private Closeable server; + + public BuilderSetup() { + + ExecutionGraphQlResponse defaultResponse = new DefaultExecutionGraphQlResponse( + ExecutionInput.newExecutionInput().query(DOCUMENT).build(), + ExecutionResultImpl.newExecutionResult().build()); + + this.responses.put(DOCUMENT, defaultResponse); + } + + public RSocketGraphQlTester.Builder initBuilder() { + + ExecutionGraphQlService graphQlService = request -> { + this.graphQlRequest = request; + String document = request.getDocument(); + ExecutionGraphQlResponse response = this.responses.get(document); + Assert.notNull(response, "Unexpected request: " + document); + return Mono.just(response); + }; + + GraphQlRSocketController controller = new GraphQlRSocketController( + new GraphQlRSocketHandler(graphQlService, Collections.emptyList(), new Jackson2JsonEncoder())); + + this.server = RSocketServer.create() + .acceptor(createSocketAcceptor(controller)) + .bind(LocalServerTransport.create("local")) + .block(); + + return RSocketGraphQlTester.builder() + .clientTransport(LocalClientTransport.create("local")); + } + + private SocketAcceptor createSocketAcceptor(GraphQlRSocketController controller) { + + RSocketStrategies.Builder builder = RSocketStrategies.builder(); + builder.encoder(new Jackson2JsonEncoder()); + builder.decoder(new Jackson2JsonDecoder()); + + RSocketMessageHandler handler = new RSocketMessageHandler(); + handler.setHandlers(Collections.singletonList(controller)); + handler.setRSocketStrategies(builder.build()); + handler.afterPropertiesSet(); + + return handler.responder(); + } + + @SuppressWarnings("unused") + public void setMockResponse(String document, ExecutionResult result) { + ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(document).build(); + this.responses.put(document, new DefaultExecutionGraphQlResponse(executionInput, result)); + } + + public GraphQlRequest getGraphQlRequest() { + return this.graphQlRequest; + } + + public void shutDown() { + if (this.server != null) { + this.server.dispose(); + } + } + } + + + @Controller + private static class GraphQlRSocketController { + + private final GraphQlRSocketHandler handler; + + GraphQlRSocketController(GraphQlRSocketHandler handler) { + this.handler = handler; + } + + @MessageMapping("graphql") + public Mono> handle(Map payload) { + return this.handler.handle(payload); + } + + @MessageMapping("graphql") + public Flux> handleSubscription(Map payload) { + return this.handler.handleSubscription(payload); + } + + } + + + private static class TestJackson2JsonDecoder extends Jackson2JsonDecoder { + + @Nullable + private Object lastValue; + + @Nullable + Object getLastValue() { + return this.lastValue; + } + + @Override + public Object decode(DataBuffer dataBuffer, ResolvableType targetType, + @Nullable MimeType mimeType, @Nullable Map hints) throws DecodingException { + + this.lastValue = super.decode(dataBuffer, targetType, mimeType, hints); + return this.lastValue; + } + + void resetLastValue() { + this.lastValue = null; + } + + } + +}