From 53fb1d4dc07544ef35e8a2c7b35df7c072ba88ab Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 4 Mar 2022 13:10:02 +0000 Subject: [PATCH] Update and expand GraphQlTester tests See gh-317 --- .../tester/GraphQlTesterBuilderTests.java | 80 +++++ .../test/tester/GraphQlTesterTestSupport.java | 99 ++++++ .../test/tester/GraphQlTesterTests.java | 192 ++++-------- .../graphql/test/tester/MovieCharacter.java | 16 +- .../test/tester/TestWebSocketClient.java | 76 +++++ .../test/tester/TestWebSocketConnection.java | 267 ++++++++++++++++ .../tester/WebGraphQlTesterBuilderTests.java | 218 +++++++++++++ .../test/tester/WebGraphQlTesterTests.java | 290 ------------------ .../src/test/resources/log4j2-test.xml | 1 + 9 files changed, 811 insertions(+), 428 deletions(-) create mode 100644 spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterBuilderTests.java create mode 100644 spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTestSupport.java create mode 100644 spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/TestWebSocketClient.java create mode 100644 spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/TestWebSocketConnection.java create mode 100644 spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterBuilderTests.java delete mode 100644 spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterTests.java diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterBuilderTests.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterBuilderTests.java new file mode 100644 index 00000000..24a789d9 --- /dev/null +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterBuilderTests.java @@ -0,0 +1,80 @@ +/* + * 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 graphql.GraphqlErrorBuilder; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.graphql.GraphQlService; +import org.springframework.graphql.support.DocumentSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link GraphQlTester} builder with a mock {@link GraphQlService}. + * + * @author Rossen Stoyanchev + */ +public class GraphQlTesterBuilderTests extends GraphQlTesterTestSupport { + + private static final String DOCUMENT = "{ Query }"; + + + @Test + void mutateDocumentSource() { + + DocumentSource documentSource = name -> name.equals("name") ? + Mono.just(DOCUMENT) : Mono.error(new IllegalArgumentException()); + + setMockResponse("{}"); + + // Original + GraphQlTester.Builder builder = graphQlTesterBuilder().documentSource(documentSource); + GraphQlTester tester = builder.build(); + tester.documentName("name").execute(); + + assertThat(requestInput().getDocument()).isEqualTo(DOCUMENT); + + // Mutate + tester = tester.mutate().build(); + tester.documentName("name").execute(); + + assertThat(requestInput().getDocument()).isEqualTo(DOCUMENT); + } + + @Test + void errorsFilteredGlobally() { + + String document = "{me {name, friends}}"; + + setMockResponse( + GraphqlErrorBuilder.newError().message("some error").build(), + GraphqlErrorBuilder.newError().message("some other error").build()); + + graphQlTesterBuilder() + .errorFilter((error) -> error.getMessage().startsWith("some ")) + .build() + .document(document) + .execute() + .errors().verify() + .path("me").pathDoesNotExist(); + + assertThat(requestInput().getDocument()).contains(document); + } + +} diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTestSupport.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTestSupport.java new file mode 100644 index 00000000..c570bcfc --- /dev/null +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTestSupport.java @@ -0,0 +1,99 @@ +/* + * 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.Arrays; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import graphql.GraphQLError; +import org.mockito.ArgumentCaptor; +import reactor.core.publisher.Mono; + +import org.springframework.graphql.GraphQlService; +import org.springframework.graphql.RequestInput; +import org.springframework.graphql.RequestOutput; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Base class for {@link GraphQlTester} tests. + * + * @author Rossen Stoyanchev + */ +public class GraphQlTesterTestSupport { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + + private final ArgumentCaptor inputCaptor = ArgumentCaptor.forClass(RequestInput.class); + + private final GraphQlService graphQlService = mock(GraphQlService.class); + + private final GraphQlTester.Builder graphQlTesterBuilder = GraphQlServiceTester.builder(this.graphQlService); + + private final GraphQlTester graphQlTester = this.graphQlTesterBuilder.build(); + + + protected GraphQlTester graphQlTester() { + return this.graphQlTester; + } + + public GraphQlTester.Builder graphQlTesterBuilder() { + return this.graphQlTesterBuilder; + } + + protected RequestInput requestInput() { + return this.inputCaptor.getValue(); + } + + + protected void setMockResponse(String data) { + setMockResponse(builder -> serialize(data, builder)); + } + + protected void setMockResponse(GraphQLError... errors) { + setMockResponse(builder -> builder.errors(Arrays.asList(errors))); + } + + private void setMockResponse(Consumer consumer) { + + ExecutionResultImpl.Builder builder = new ExecutionResultImpl.Builder(); + consumer.accept(builder); + ExecutionInput executionInput = ExecutionInput.newExecutionInput("{}").build(); + ExecutionResult result = builder.build(); + + given(this.graphQlService.execute(this.inputCaptor.capture())) + .willReturn(Mono.just(new RequestOutput(executionInput, result))); + } + + private void serialize(String data, ExecutionResultImpl.Builder builder) { + try { + builder.data(OBJECT_MAPPER.readValue(data, Map.class)); + } + catch (JsonProcessingException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java index d7eecc5b..abb6b9ea 100644 --- a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java @@ -16,77 +16,51 @@ package org.springframework.graphql.test.tester; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import graphql.ExecutionInput; -import graphql.ExecutionResult; -import graphql.ExecutionResultImpl; -import graphql.GraphQLError; import graphql.GraphqlErrorBuilder; import graphql.language.SourceLocation; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; import org.springframework.graphql.GraphQlService; import org.springframework.graphql.RequestInput; -import org.springframework.graphql.RequestOutput; -import org.springframework.lang.Nullable; -import org.springframework.util.CollectionUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; /** - * Tests for {@link GraphQlTester}. + * Tests for {@link GraphQlTester} with a mock {@link GraphQlService}. * - *

- * There is no actual handling via {@link graphql.GraphQL} in either scenario. The main - * focus is to verify {@link GraphQlTester} request preparation and response handling. + * @author Rossen Stoyanchev */ -public class GraphQlTesterTests { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - - private final GraphQlService service = mock(GraphQlService.class); - - private final GraphQlTester graphQlTester = GraphQlTester.create(this.service); - - private final ArgumentCaptor inputCaptor = ArgumentCaptor.forClass(RequestInput.class); - +public class GraphQlTesterTests extends GraphQlTesterTestSupport { @Test - void pathAndValueExist() throws Exception { + void pathAndValueExist() { String document = "{me {name, friends}}"; - setResponse("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}"); + setMockResponse("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}"); - GraphQlTester.ResponseSpec spec = this.graphQlTester.query(document).execute(); + GraphQlTester.ResponseSpec spec = graphQlTester().document(document).execute(); spec.path("me.name").pathExists().valueExists(); spec.path("me.friends").pathExists().valueExists(); spec.path("hero").pathDoesNotExist().valueDoesNotExist(); - assertThat(this.inputCaptor.getValue().getDocument()).contains(document); + assertThat(requestInput().getDocument()).contains(document); } @Test - void valueIsEmpty() throws Exception { + void valueIsEmpty() { String document = "{me {name, friends}}"; - setResponse("{\"me\": {\"name\":null, \"friends\":[]}}"); + setMockResponse("{\"me\": {\"name\":null, \"friends\":[]}}"); - GraphQlTester.ResponseSpec spec = this.graphQlTester.query(document).execute(); + GraphQlTester.ResponseSpec spec = graphQlTester().document(document).execute(); spec.path("me.name").valueIsEmpty(); spec.path("me.friends").valueIsEmpty(); @@ -95,16 +69,16 @@ public class GraphQlTesterTests { .as("Path does not even exist") .hasMessageContaining("No value at JSON path \"$['data']['hero']"); - assertThat(this.inputCaptor.getValue().getDocument()).contains(document); + assertThat(requestInput().getDocument()).contains(document); } @Test - void matchesJson() throws Exception { + void matchesJson() { String document = "{me {name}}"; - setResponse("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}"); + setMockResponse("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}"); - GraphQlTester.ResponseSpec spec = this.graphQlTester.query(document).execute(); + GraphQlTester.ResponseSpec spec = graphQlTester().document(document).execute(); spec.path("").matchesJson("{\"me\": {\"name\":\"Luke Skywalker\",\"friends\":[]}}"); spec.path("me").matchesJson("{\"name\":\"Luke Skywalker\"}"); @@ -115,29 +89,29 @@ public class GraphQlTesterTests { .as("Extended fields should fail in strict mode") .hasMessageContaining("Unexpected: name"); - assertThat(this.inputCaptor.getValue().getDocument()).contains(document); + assertThat(requestInput().getDocument()).contains(document); } @Test - void entity() throws Exception { + void entity() { String document = "{me {name}}"; - setResponse("{\"me\": {\"name\":\"Luke Skywalker\"}}"); + setMockResponse("{\"me\": {\"name\":\"Luke Skywalker\"}}"); - GraphQlTester.ResponseSpec spec = this.graphQlTester.query(document).execute(); + GraphQlTester.ResponseSpec spec = graphQlTester().document(document).execute(); MovieCharacter luke = MovieCharacter.create("Luke Skywalker"); MovieCharacter han = MovieCharacter.create("Han Solo"); AtomicReference personRef = new AtomicReference<>(); - MovieCharacter actual = spec.path("me") - .entity(MovieCharacter.class) + MovieCharacter actual = spec.path("me").entity(MovieCharacter.class) .isEqualTo(luke) .isNotEqualTo(han) .satisfies(personRef::set) .matches((movieCharacter) -> personRef.get().equals(movieCharacter)) .isSameAs(personRef.get()) - .isNotSameAs(luke).get(); + .isNotSameAs(luke) + .get(); assertThat(actual.getName()).isEqualTo("Luke Skywalker"); @@ -145,28 +119,27 @@ public class GraphQlTesterTests { .entity(new ParameterizedTypeReference>() {}) .isEqualTo(Collections.singletonMap("me", luke)); - assertThat(this.inputCaptor.getValue().getDocument()).contains(document); + assertThat(requestInput().getDocument()).contains(document); } @Test - void entityList() throws Exception { + void entityList() { String document = "{me {name, friends}}"; - setResponse("{" + + setMockResponse("{" + " \"me\":{" + " \"name\":\"Luke Skywalker\"," + " \"friends\":[{\"name\":\"Han Solo\"}, {\"name\":\"Leia Organa\"}]" + " }" + "}"); - GraphQlTester.ResponseSpec spec = this.graphQlTester.query(document).execute(); + GraphQlTester.ResponseSpec spec = graphQlTester().document(document).execute(); MovieCharacter han = MovieCharacter.create("Han Solo"); MovieCharacter leia = MovieCharacter.create("Leia Organa"); MovieCharacter jabba = MovieCharacter.create("Jabba the Hutt"); - List actual = spec.path("me.friends") - .entityList(MovieCharacter.class) + List actual = spec.path("me.friends").entityList(MovieCharacter.class) .contains(han) .containsExactly(han, leia) .doesNotContain(jabba) @@ -181,11 +154,11 @@ public class GraphQlTesterTests { .entityList(new ParameterizedTypeReference() {}) .containsExactly(han, leia); - assertThat(this.inputCaptor.getValue().getDocument()).contains(document); + assertThat(requestInput().getDocument()).contains(document); } @Test - void operationNameAndVariables() throws Exception { + void operationNameAndVariables() { String document = "query HeroNameAndFriends($episode: Episode) {" + " hero(episode: $episode) {" + @@ -193,9 +166,9 @@ public class GraphQlTesterTests { + " }" + "}"; - setResponse("{\"hero\": {\"name\":\"R2-D2\"}}"); + setMockResponse("{\"hero\": {\"name\":\"R2-D2\"}}"); - GraphQlTester.ResponseSpec spec = this.graphQlTester.query(document) + GraphQlTester.ResponseSpec spec = graphQlTester().document(document) .operationName("HeroNameAndFriends") .variable("episode", "JEDI") .variable("foo", "bar") @@ -204,7 +177,7 @@ public class GraphQlTesterTests { spec.path("hero").entity(MovieCharacter.class).isEqualTo(MovieCharacter.create("R2-D2")); - RequestInput input = this.inputCaptor.getValue(); + RequestInput input = requestInput(); assertThat(input.getDocument()).contains(document); assertThat(input.getOperationName()).isEqualTo("HeroNameAndFriends"); assertThat(input.getVariables()).hasSize(3); @@ -214,57 +187,57 @@ public class GraphQlTesterTests { } @Test - void errorsCheckedOnExecuteAndVerify() throws Exception { + void errorsCheckedOnExecuteAndVerify() { String document = "{me {name, friends}}"; - setResponse(GraphqlErrorBuilder.newError().message("Invalid query").build()); + setMockResponse(GraphqlErrorBuilder.newError().message("Invalid query").build()); - assertThatThrownBy(() -> this.graphQlTester.query(document).executeAndVerify()) + assertThatThrownBy(() -> graphQlTester().document(document).executeAndVerify()) .hasMessageContaining("Response has 1 unexpected error(s)."); - assertThat(this.inputCaptor.getValue().getDocument()).contains(document); + assertThat(requestInput().getDocument()).contains(document); } @Test - void errorsCheckedOnTraverse() throws Exception { + void errorsCheckedOnTraverse() { String document = "{me {name, friends}}"; - setResponse(GraphqlErrorBuilder.newError().message("Invalid query").build()); + setMockResponse(GraphqlErrorBuilder.newError().message("Invalid query").build()); - assertThatThrownBy(() -> this.graphQlTester.query(document).execute().path("me")) + assertThatThrownBy(() -> graphQlTester().document(document).execute().path("me")) .hasMessageContaining("Response has 1 unexpected error(s)."); - assertThat(this.inputCaptor.getValue().getDocument()).contains(document); + assertThat(requestInput().getDocument()).contains(document); } @Test - void errorsPartiallyFiltered() throws Exception { + void errorsPartiallyFiltered() { String document = "{me {name, friends}}"; - setResponse( + setMockResponse( GraphqlErrorBuilder.newError().message("some error").build(), GraphqlErrorBuilder.newError().message("some other error").build()); assertThatThrownBy(() -> - this.graphQlTester.query(document) + graphQlTester().document(document) .execute() .errors() .filter((error) -> error.getMessage().equals("some error")) .verify()) .hasMessageContaining("Response has 1 unexpected error(s) of 2 total."); - assertThat(this.inputCaptor.getValue().getDocument()).contains(document); + assertThat(requestInput().getDocument()).contains(document); } @Test - void errorsFiltered() throws Exception { + void errorsFiltered() { String document = "{me {name, friends}}"; - setResponse( + setMockResponse( GraphqlErrorBuilder.newError().message("some error").build(), GraphqlErrorBuilder.newError().message("some other error").build()); - this.graphQlTester.query(document) + graphQlTester().document(document) .execute() .errors() .filter((error) -> error.getMessage().startsWith("some ")) @@ -272,74 +245,52 @@ public class GraphQlTesterTests { .path("me") .pathDoesNotExist(); - assertThat(this.inputCaptor.getValue().getDocument()).contains(document); + assertThat(requestInput().getDocument()).contains(document); } @Test - void errorsFilteredGlobally() throws Exception { + void errorsExpected() { String document = "{me {name, friends}}"; - setResponse( + setMockResponse( GraphqlErrorBuilder.newError().message("some error").build(), GraphqlErrorBuilder.newError().message("some other error").build()); - GraphQlTester.builder(this.service) - .errorFilter((error) -> error.getMessage().startsWith("some ")) - .build() - .query(document) - .execute() - .errors() - .verify() - .path("me") - .pathDoesNotExist(); - - assertThat(this.inputCaptor.getValue().getDocument()).contains(document); - } - - @Test - void errorsExpected() throws Exception { - - String document = "{me {name, friends}}"; - setResponse( - GraphqlErrorBuilder.newError().message("some error").build(), - GraphqlErrorBuilder.newError().message("some other error").build()); - - this.graphQlTester.query(document) + graphQlTester().document(document) .execute() .errors() .expect((error) -> error.getMessage().startsWith("some ")) .verify() - .path("me") - .pathDoesNotExist(); + .path("me").pathDoesNotExist(); - assertThat(this.inputCaptor.getValue().getDocument()).contains(document); + assertThat(requestInput().getDocument()).contains(document); } @Test - void errorsExpectedButNotFound() throws Exception { + void errorsExpectedButNotFound() { String document = "{me {name, friends}}"; - setResponse( + setMockResponse( GraphqlErrorBuilder.newError().message("some error").build(), GraphqlErrorBuilder.newError().message("some other error").build()); assertThatThrownBy(() -> - this.graphQlTester.query(document) + graphQlTester().document(document) .execute() .errors().expect((error) -> error.getMessage().startsWith("another "))) .hasMessageStartingWith("No matching errors."); } @Test - void errorsConsumed() throws Exception { + void errorsConsumed() { String document = "{me {name, friends}}"; - setResponse(GraphqlErrorBuilder.newError() + setMockResponse(GraphqlErrorBuilder.newError() .message("Invalid query") .location(new SourceLocation(1, 2)) .build()); - this.graphQlTester.query(document) + graphQlTester().document(document) .execute() .errors() .satisfy((errors) -> { @@ -349,32 +300,9 @@ public class GraphQlTesterTests { assertThat(errors.get(0).getLocations().get(0).getLine()).isEqualTo(1); assertThat(errors.get(0).getLocations().get(0).getColumn()).isEqualTo(2); }) - .path("me") - .pathDoesNotExist(); + .path("me").pathDoesNotExist(); - assertThat(this.inputCaptor.getValue().getDocument()).contains(document); - } - - private void setResponse(String data) throws Exception { - setResponse(data, Collections.emptyList()); - } - - private void setResponse(GraphQLError... errors) throws Exception { - setResponse(null, Arrays.asList(errors)); - } - - private void setResponse(@Nullable String data, List errors) throws Exception { - ExecutionResultImpl.Builder builder = new ExecutionResultImpl.Builder(); - if (data != null) { - builder.data(OBJECT_MAPPER.readValue(data, new TypeReference>() {})); - } - if (!CollectionUtils.isEmpty(errors)) { - builder.addErrors(errors); - } - ExecutionInput executionInput = ExecutionInput.newExecutionInput("{}").build(); - ExecutionResult result = builder.build(); - given(this.service.execute(this.inputCaptor.capture())) - .willReturn(Mono.just(new RequestOutput(executionInput, result))); + assertThat(requestInput().getDocument()).contains(document); } } diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/MovieCharacter.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/MovieCharacter.java index bad8b1ec..ae044648 100644 --- a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/MovieCharacter.java +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/MovieCharacter.java @@ -17,12 +17,15 @@ package org.springframework.graphql.test.tester; import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + public class MovieCharacter { @Nullable private String name; + public void setName(String name) { this.name = name; } @@ -32,12 +35,14 @@ public class MovieCharacter { return this.name; } + public static MovieCharacter create(String name) { - MovieCharacter movieCharacter = new MovieCharacter(); - movieCharacter.setName(name); - return movieCharacter; + MovieCharacter character = new MovieCharacter(); + character.setName(name); + return character; } + @Override public boolean equals(Object other) { if (this == other) { @@ -46,13 +51,12 @@ public class MovieCharacter { if (other == null || getClass() != other.getClass()) { return false; } - MovieCharacter movieCharacter = (MovieCharacter) other; - return (this.name != null) ? this.name.equals(movieCharacter.name) : movieCharacter.name == null; + return ObjectUtils.nullSafeEquals(this.name, ((MovieCharacter) other).name); } @Override public int hashCode() { - return (this.name != null) ? this.name.hashCode() : 0; + return (this.name != null) ? this.name.hashCode() : super.hashCode(); } } diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/TestWebSocketClient.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/TestWebSocketClient.java new file mode 100644 index 00000000..b7fa1c37 --- /dev/null +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/TestWebSocketClient.java @@ -0,0 +1,76 @@ +/* + * 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.concurrent.CopyOnWriteArrayList; + +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.Assert; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.client.WebSocketClient; + +/** + * Copy of same class in spring-graphql tests. + */ +final class TestWebSocketClient implements WebSocketClient { + + private final WebSocketHandler serverHandler; + + private final List connections = new CopyOnWriteArrayList<>(); + + + public TestWebSocketClient(WebSocketHandler serverHandler) { + this.serverHandler = serverHandler; + } + + + /** + * Return the connection at the specified index from a list of connections + * based on order of execution. + */ + public TestWebSocketConnection getConnection(int index) { + Assert.isTrue(index < this.connections.size(), + "No connection at index=" + index + ", total=" + this.connections.size()); + return connections.get(index); + } + + /** + * Return the number of connections which corresponds to the number of calls + * to one of the execute methods. + */ + public int getConnectionCount() { + return this.connections.size(); + } + + + @Override + public Mono execute(URI url, WebSocketHandler clientHandler) { + return execute(URI.create("/"), HttpHeaders.EMPTY, clientHandler); + } + + @Override + public Mono execute(URI url, HttpHeaders headers, WebSocketHandler clientHandler) { + TestWebSocketConnection connection = new TestWebSocketConnection(url, headers); + this.connections.add(connection); + return connection.connect(clientHandler, this.serverHandler); + } + +} diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/TestWebSocketConnection.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/TestWebSocketConnection.java new file mode 100644 index 00000000..289fd765 --- /dev/null +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/TestWebSocketConnection.java @@ -0,0 +1,267 @@ +/* + * 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.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicLong; + +import org.reactivestreams.Publisher; +import reactor.core.Scannable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.web.reactive.socket.CloseStatus; +import org.springframework.web.reactive.socket.HandshakeInfo; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.WebSocketMessage; +import org.springframework.web.reactive.socket.adapter.AbstractWebSocketSession; + +/** + * Copy of same class in spring-graphql tests. + */ +final class TestWebSocketConnection { + + private static final AtomicLong connectionIndex = new AtomicLong(); + + + private final URI url; + + private final HttpHeaders headers; + + private final TestWebSocketSession clientSession; + + private final TestWebSocketSession serverSession; + + + public TestWebSocketConnection(URI url, HttpHeaders headers) { + + this.url = url; + this.headers = headers; + + long id = connectionIndex.incrementAndGet(); + + Sinks.Many clientSink = Sinks.many().unicast().onBackpressureBuffer(); + Sinks.Many serverSink = Sinks.many().unicast().onBackpressureBuffer(); + + Sinks.One clientStatusSink = Sinks.one(); + Sinks.One serverStatusSink = Sinks.one(); + + this.clientSession = new TestWebSocketSession("client-session-" + id, url, headers, + clientSink, serverSink.asFlux(), clientStatusSink, serverStatusSink.asMono()); + + this.serverSession = new TestWebSocketSession("server-session-" + id, url, headers, + serverSink, clientSink.asFlux(), serverStatusSink, clientStatusSink.asMono()); + } + + + public URI getUrl() { + return this.url; + } + + public HttpHeaders getHeaders() { + return this.headers; + } + + /** + * Return {@code true} if both client and server sessions are open. + */ + public boolean isOpen() { + return (this.clientSession.isOpen() && this.serverSession.isOpen()); + } + + /** + * Return messages sent from the client side. + */ + public List getClientMessages() { + return this.clientSession.getSentMessages(); + } + + /** + * Return messages sent from the server side. + */ + public List getServerMessages() { + return this.serverSession.getSentMessages(); + } + + + /** + * Starts client and server session handling, and if either side errors or + * completes, close its session with either {@link CloseStatus#NORMAL} or + * {@link CloseStatus#PROTOCOL_ERROR} respectively. + * @param clientHandler the client session handler + * @param serverHandler the server session handler + * @return {@code Mono} that completes when either client or server + * session handling completes or when either is closed + */ + Mono connect(WebSocketHandler clientHandler, WebSocketHandler serverHandler) { + + Mono serverMono = invokeHandler(serverHandler, this.serverSession); + Mono clientMono = invokeHandler(clientHandler, this.clientSession); + + Mono serverStatusMono = this.serverSession.closeStatus().then(); + Mono clientStatusMono = this.clientSession.closeStatus().then(); + + return Mono.zip(serverMono, clientMono, serverStatusMono, clientStatusMono).then(); + } + + /** + * Handle the session and complete it when handling completes. + */ + private Mono invokeHandler(WebSocketHandler serverHandler, TestWebSocketSession session) { + return serverHandler.handle(session) + .then(Mono.defer(() -> session.close(CloseStatus.NORMAL))) + .onErrorResume(ex -> session.close(CloseStatus.PROTOCOL_ERROR).then(Mono.error(ex))); + } + + + /** + * Close the connection from the client side. + */ + public Mono closeClientSession(CloseStatus status) { + return this.clientSession.close(status); + } + + /** + * Close the connection from the server side. + */ + public Mono closeServerSession(CloseStatus status) { + return this.serverSession.close(status); + } + + /** + * Return the {@code CloseStatus} that may have come from either side. + */ + public Mono closeStatus() { + return this.clientSession.closeStatus().or(this.serverSession.closeStatus()); + } + + + /** + * Test WebSocketSession that sends to a given {@link Sinks.Many sink} and + * receives from a given {@code Flux}. + */ + private static class TestWebSocketSession extends AbstractWebSocketSession { + + private final Sinks.Many sendSink; + + private final Flux receiveFlux; + + private final Sinks.One closeStatusSink; + + private final Queue sentMessages = new ConcurrentLinkedQueue<>(); + + + TestWebSocketSession(String sessionId, URI url, HttpHeaders headers, + Sinks.Many sendSink, Flux receiveFlux, + Sinks.One closeStatusSink, Mono remoteCloseStatusMono) { + + super(new Object(), sessionId, new HandshakeInfo(url, headers, Mono.empty(), null), + DefaultDataBufferFactory.sharedInstance); + + this.sendSink = sendSink; + this.receiveFlux = receiveFlux.cache(); + this.closeStatusSink = closeStatusSink; + + // Close this side when the remote closes + remoteCloseStatusMono.doOnSuccess(this::handleRemoteClosure).subscribe(); + } + + private void handleRemoteClosure(@Nullable CloseStatus status) { + + if (!isOpen()) { + // when we close, remote closes, and we detect that + return; + } + + if (logger.isDebugEnabled()) { + logger.debug("Closing " + this + " due to remote " + status); + } + + closeInternal(status); + } + + + public List getSentMessages() { + return new ArrayList<>(this.sentMessages); + } + + @Override + public Mono send(Publisher messages) { + return Flux.from(messages) + .doOnNext(this::saveMessage) + .doOnNext(message -> { + Sinks.EmitResult result = this.sendSink.tryEmitNext(message); + Assert.state(result.isSuccess(), this + " failed to send: " + message + ", with " + result); + }) + .then(); + } + + private void saveMessage(WebSocketMessage message) { + DataBuffer payload = message.getPayload().retainedSlice(0, message.getPayload().readableByteCount()); + this.sentMessages.add(new WebSocketMessage(message.getType(), payload)); + } + + @Override + public Flux receive() { + return this.receiveFlux; + } + + @Override + public boolean isOpen() { + return !Boolean.TRUE.equals(this.closeStatusSink.scan(Scannable.Attr.TERMINATED)); + } + + @Override + public Mono closeStatus() { + return this.closeStatusSink.asMono(); + } + + public Mono close(CloseStatus status) { + if (logger.isDebugEnabled()) { + logger.debug("Closing " + this + " with " + status); + } + closeInternal(status); + return Mono.empty(); + } + + private void closeInternal(@Nullable CloseStatus status) { + if (status != null) { + this.closeStatusSink.tryEmitValue(status); + } + else { + this.closeStatusSink.tryEmitEmpty(); + }; + } + + @Override + public String toString() { + return getId(); + } + + } + +} diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterBuilderTests.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterBuilderTests.java new file mode 100644 index 00000000..64646309 --- /dev/null +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterBuilderTests.java @@ -0,0 +1,218 @@ +/* + * 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.time.Duration; +import java.util.stream.Stream; + +import graphql.ExecutionInput; +import graphql.ExecutionResultImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.publisher.Mono; + +import org.springframework.graphql.RequestOutput; +import org.springframework.graphql.support.DocumentSource; +import org.springframework.graphql.web.WebGraphQlHandler; +import org.springframework.graphql.web.WebInput; +import org.springframework.graphql.web.WebInterceptor; +import org.springframework.graphql.web.WebOutput; +import org.springframework.graphql.web.webflux.GraphQlHttpHandler; +import org.springframework.graphql.web.webflux.GraphQlWebSocketHandler; +import org.springframework.http.codec.ClientCodecConfigurer; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.reactive.socket.WebSocketHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + + +/** + * Tests for the builders of Web {@code GraphQlTester} extensions, using a + * {@link WebInterceptor} to capture the WebInput on the server side, and return + * with no handling. + * + *
    + *
  • {@link HttpGraphQlTester} via {@link WebTestClient} to {@link GraphQlHttpHandler} + *
  • {@link WebSocketGraphQlTester} via a {@link TestWebSocketConnection} to {@link GraphQlWebSocketHandler} + *
  • {@link WebGraphQlTester} via direct call to {@link WebGraphQlHandler} + *
+ * + * @author Rossen Stoyanchev + */ +public class WebGraphQlTesterBuilderTests { + + private static final String DOCUMENT = "{ Query }"; + + + public static Stream argumentSource() { + return Stream.of(new WebBuilderSetup(), new HttpBuilderSetup(), new WebSocketBuilderSetup()); + } + + + @ParameterizedTest + @MethodSource("argumentSource") + void mutateUrlHeaders(TesterBuilderSetup builderSetup) { + + String url = "/graphql-one"; + + // Original + WebGraphQlTester.Builder builder = builderSetup.initBuilder() + .url(url) + .headers(headers -> headers.add("h", "one")); + + WebGraphQlTester tester = builder.build(); + tester.document(DOCUMENT).execute(); + + WebInput input = builderSetup.getWebInput(); + assertThat(input.getUri().toString()).isEqualTo(url); + assertThat(input.getHeaders().get("h")).containsExactly("one"); + + // Mutate to add header value + builder = tester.mutate().headers(headers -> headers.add("h", "two")); + tester = builder.build(); + tester.document(DOCUMENT).execute(); + assertThat(builderSetup.getWebInput().getHeaders().get("h")).containsExactly("one", "two"); + + // Mutate to replace header + builder = tester.mutate().header("h", "three", "four"); + tester = builder.build(); + tester.document(DOCUMENT).execute(); + + input = builderSetup.getWebInput(); + assertThat(input.getUri().toString()).isEqualTo(url); + assertThat(input.getHeaders().get("h")).containsExactly("three", "four"); + } + + @Test + void mutateWebTestClientViaConsumer() { + HttpBuilderSetup testerSetup = new HttpBuilderSetup(); + + // Original header value + HttpGraphQlTester.Builder builder = testerSetup.initBuilder() + .webTestClient(testClientBuilder -> testClientBuilder.defaultHeaders(h -> h.add("h", "one"))); + + HttpGraphQlTester tester = builder.build(); + tester.document(DOCUMENT).execute(); + assertThat(testerSetup.getWebInput().getHeaders().get("h")).containsExactly("one"); + + // Mutate to add header value + HttpGraphQlTester.Builder builder2 = tester.mutate() + .webTestClient(testClientBuilder -> testClientBuilder.defaultHeaders(h -> h.add("h", "two"))); + + tester = builder2.build(); + tester.document(DOCUMENT).execute(); + assertThat(testerSetup.getWebInput().getHeaders().get("h")).containsExactly("one", "two"); + + // Mutate to replace header + HttpGraphQlTester.Builder builder3 = tester.mutate() + .webTestClient(testClientBuilder -> testClientBuilder.defaultHeader("h", "three")); + + tester = builder3.build(); + tester.document(DOCUMENT).execute(); + assertThat(testerSetup.getWebInput().getHeaders().get("h")).containsExactly("three"); + } + + @ParameterizedTest + @MethodSource("argumentSource") + void mutateDocumentSource(TesterBuilderSetup builderSetup) { + + DocumentSource documentSource = name -> name.equals("name") ? + Mono.just(DOCUMENT) : Mono.error(new IllegalArgumentException()); + + // Original + WebGraphQlTester.Builder builder = builderSetup.initBuilder().documentSource(documentSource); + WebGraphQlTester tester = builder.build(); + tester.documentName("name").execute(); + + WebInput input = builderSetup.getWebInput(); + assertThat(input.getDocument()).isEqualTo(DOCUMENT); + + // Mutate + tester = tester.mutate().build(); + tester.documentName("name").execute(); + + input = builderSetup.getWebInput(); + assertThat(input.getDocument()).isEqualTo(DOCUMENT); + } + + + private interface TesterBuilderSetup { + + WebGraphQlTester.Builder initBuilder(); + + WebInput getWebInput(); + + } + + + private static class WebBuilderSetup implements TesterBuilderSetup { + + private WebInput webInput; + + @Override + public WebGraphQlTester.Builder initBuilder() { + return WebGraphQlTester.builder(webGraphQlHandler()); + } + + protected WebGraphQlHandler webGraphQlHandler() { + return WebGraphQlHandler.builder(requestInput -> Mono.error(new UnsupportedOperationException())) + .interceptor((input, chain) -> { + this.webInput = input; + return Mono.just(new WebOutput(new RequestOutput( + ExecutionInput.newExecutionInput().query("{ notUsed }").build(), + ExecutionResultImpl.newExecutionResult().build()))); + }) + .build(); + } + + @Override + public WebInput getWebInput() { + return this.webInput; + } + + } + + + private static class HttpBuilderSetup extends WebBuilderSetup { + + @Override + public HttpGraphQlTester.Builder initBuilder() { + GraphQlHttpHandler handler = new GraphQlHttpHandler(webGraphQlHandler()); + RouterFunction routerFunction = route().POST("/**", handler::handleRequest).build(); + return HttpGraphQlTester.builder(WebTestClient.bindToRouterFunction(routerFunction).configureClient()); + } + + } + + + private static class WebSocketBuilderSetup extends WebBuilderSetup { + + @Override + public WebSocketGraphQlTester.Builder initBuilder() { + ClientCodecConfigurer configurer = ClientCodecConfigurer.create(); + WebSocketHandler handler = new GraphQlWebSocketHandler(webGraphQlHandler(), configurer, Duration.ofSeconds(5)); + return WebSocketGraphQlTester.builder(URI.create(""), new TestWebSocketClient(handler)); + } + + } + +} diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterTests.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterTests.java deleted file mode 100644 index 8ad45321..00000000 --- a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterTests.java +++ /dev/null @@ -1,290 +0,0 @@ -/* - * 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.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import graphql.ExecutionInput; -import graphql.ExecutionResult; -import graphql.ExecutionResultImpl; -import graphql.GraphQLError; -import graphql.GraphqlErrorBuilder; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.ArgumentCaptor; -import reactor.core.publisher.Mono; - -import org.springframework.graphql.RequestOutput; -import org.springframework.graphql.web.WebGraphQlHandler; -import org.springframework.graphql.web.WebInput; -import org.springframework.graphql.web.WebOutput; -import org.springframework.http.HttpHeaders; -import org.springframework.lang.Nullable; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link WebGraphQlTester} parameterized to: - *
    - *
  • Connect to {@link MockWebServer} and return a preset HTTP response. - *
  • Use mock {@link WebGraphQlHandler} to return a preset {@link ExecutionResult}. - *
- * - *

- * There is no actual handling via {@link graphql.GraphQL} in either scenario. - * The main focus is to verify {@link GraphQlTester} request preparation and - * response handling. - */ -public class WebGraphQlTesterTests { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - public static Stream argumentSource() { - return Stream.of(new MockWebServerSetup(), new MockWebGraphQlHandlerSetup()); - } - - - @ParameterizedTest - @MethodSource("argumentSource") - void headers(GraphQlTesterSetup setup) throws Exception { - - String query = "{me {name, friends}}"; - setup.response("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}"); - - GraphQlTester.ResponseSpec spec = setup.graphQlTester().query(query) - .httpHeader("myHeader1", "myValue1a") - .httpHeader("myHeader1", "myValue1b") - .httpHeaders(headers -> headers.add("myHeader2", "myValue2")) - .execute(); - - spec.path("me.name").entity(String.class).isEqualTo("Luke Skywalker"); - - setup.verifyRequest((input) -> { - assertThat(input.getDocument()).contains(query); - assertThat(input.getHeaders().get("myHeader1")).containsExactly("myValue1a", "myValue1b"); - assertThat(input.getHeaders().getFirst("myHeader2")).isEqualTo("myValue2"); - }); - setup.shutdown(); - } - - @ParameterizedTest - @MethodSource("argumentSource") - void defaultHeaders(GraphQlTesterSetup setup) throws Exception { - - String query = "{me {name, friends}}"; - setup.response("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}"); - - GraphQlTester.ResponseSpec spec = setup.graphQlTesterBuilder() - .defaultHttpHeader("myHeader1", "myValue1a") - .defaultHttpHeader("myHeader1", "myValue1b") - .defaultHttpHeaders(headers -> headers.add("myHeader2", "myValue2")) - .build() - .query(query) - .execute(); - - spec.path("me.name").entity(String.class).isEqualTo("Luke Skywalker"); - - setup.verifyRequest((input) -> { - assertThat(input.getDocument()).contains(query); - assertThat(input.getHeaders().get("myHeader1")).containsExactly("myValue1a", "myValue1b"); - assertThat(input.getHeaders().getFirst("myHeader2")).isEqualTo("myValue2"); - }); - - setup.shutdown(); - } - - @ParameterizedTest - @MethodSource("argumentSource") - void errorsFilteredGlobally(GraphQlTesterSetup setup) throws Exception { - - String query = "{me {name, friends}}"; - setup.response( - GraphqlErrorBuilder.newError().message("some error").build(), - GraphqlErrorBuilder.newError().message("some other error").build()); - - setup.graphQlTesterBuilder() - .errorFilter((error) -> error.getMessage().startsWith("some ")) - .build() - .query(query) - .execute() - .errors() - .verify() - .path("me") - .pathDoesNotExist(); - - setup.verifyRequest((input) -> assertThat(input.getDocument()).contains(query)); - } - - - private interface GraphQlTesterSetup { - - WebGraphQlTester graphQlTester(); - - WebGraphQlTester.Builder graphQlTesterBuilder(); - - default void response(String data) throws Exception { - response(data, Collections.emptyList()); - } - - default void response(GraphQLError... errors) throws Exception { - response(null, Arrays.asList(errors)); - } - - void response(@Nullable String data, List errors) throws Exception; - - void verifyRequest(Consumer consumer) throws Exception; - - default void shutdown() throws Exception { - // no-op by default - } - - } - - private static class MockWebServerSetup implements GraphQlTesterSetup { - - private final MockWebServer server; - - private final WebGraphQlTester.Builder graphQlTesterBuilder; - - MockWebServerSetup() { - this.server = new MockWebServer(); - this.graphQlTesterBuilder = WebGraphQlTester.builder(initWebTestClient(this.server)); - } - - private static WebTestClient initWebTestClient(MockWebServer server) { - String baseUrl = server.url("/graphQL").toString(); - return WebTestClient.bindToServer().baseUrl(baseUrl).build(); - } - - @Override - public WebGraphQlTester graphQlTester() { - return this.graphQlTesterBuilder.build(); - } - - @Override - public WebGraphQlTester.Builder graphQlTesterBuilder() { - return this.graphQlTesterBuilder; - } - - @Override - public void response(@Nullable String data, List errors) throws Exception { - StringBuilder sb = new StringBuilder("{"); - if (StringUtils.hasText(data)) { - sb.append("\"data\":").append(data); - } - if (!CollectionUtils.isEmpty(errors)) { - List> errorSpecs = errors.stream().map(GraphQLError::toSpecification) - .collect(Collectors.toList()); - - sb.append(StringUtils.hasText(data) ? ", " : "").append("\"errors\":") - .append(OBJECT_MAPPER.writeValueAsString(errorSpecs)); - } - sb.append("}"); - - MockResponse response = new MockResponse(); - response.setHeader("Content-Type", "application/json;charset=UTF-8"); // charset should be ignored if present - response.setBody(sb.toString()); - - this.server.enqueue(response); - } - - @Override - public void verifyRequest(Consumer consumer) throws Exception { - assertThat(this.server.getRequestCount()).isEqualTo(1); - RecordedRequest request = this.server.takeRequest(); - assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("application/json"); - - HttpHeaders headers = new HttpHeaders(); - request.getHeaders().names().forEach(name -> headers.put(name, request.getHeaders().values(name))); - - String content = request.getBody().readUtf8(); - Map map = new ObjectMapper().readValue(content, new TypeReference>() {}); - WebInput webInput = new WebInput(request.getRequestUrl().uri(), headers, map, null, ObjectUtils.getIdentityHexString(request)); - - consumer.accept(webInput); - } - - @Override - public void shutdown() throws Exception { - this.server.shutdown(); - } - - } - - private static class MockWebGraphQlHandlerSetup implements GraphQlTesterSetup { - - private final WebGraphQlHandler handler = mock(WebGraphQlHandler.class); - - private final ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(WebInput.class); - - private final WebGraphQlTester.Builder graphQlTesterBuilder; - - MockWebGraphQlHandlerSetup() { - this.graphQlTesterBuilder = WebGraphQlTester.builder(this.handler); - } - - @Override - public WebGraphQlTester graphQlTester() { - return this.graphQlTesterBuilder.build(); - } - - @Override - public WebGraphQlTester.Builder graphQlTesterBuilder() { - return this.graphQlTesterBuilder; - } - - @Override - public void response(@Nullable String data, List errors) throws Exception { - ExecutionResultImpl.Builder builder = new ExecutionResultImpl.Builder(); - if (data != null) { - builder.data(OBJECT_MAPPER.readValue(data, new TypeReference>() {})); - } - if (!CollectionUtils.isEmpty(errors)) { - builder.addErrors(errors); - } - ExecutionInput executionInput = ExecutionInput.newExecutionInput("{}").build(); - ExecutionResult result = builder.build(); - WebOutput output = new WebOutput(new RequestOutput(executionInput, result)); - given(this.handler.handleRequest(this.bodyCaptor.capture())).willReturn(Mono.just(output)); - } - - @Override - public void verifyRequest(Consumer consumer) { - WebInput webInput = this.bodyCaptor.getValue(); - consumer.accept(webInput); - } - - } - -} diff --git a/spring-graphql-test/src/test/resources/log4j2-test.xml b/spring-graphql-test/src/test/resources/log4j2-test.xml index 950a361a..d7fc6608 100644 --- a/spring-graphql-test/src/test/resources/log4j2-test.xml +++ b/spring-graphql-test/src/test/resources/log4j2-test.xml @@ -8,6 +8,7 @@ +