Update and expand GraphQlTester tests
See gh-317
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<RequestInput> 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<ExecutionResultImpl.Builder> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}.
|
||||
*
|
||||
* <p>
|
||||
* 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<RequestInput> 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<MovieCharacter> 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<Map<String, MovieCharacter>>() {})
|
||||
.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<MovieCharacter> actual = spec.path("me.friends")
|
||||
.entityList(MovieCharacter.class)
|
||||
List<MovieCharacter> 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<MovieCharacter>() {})
|
||||
.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<GraphQLError> errors) throws Exception {
|
||||
ExecutionResultImpl.Builder builder = new ExecutionResultImpl.Builder();
|
||||
if (data != null) {
|
||||
builder.data(OBJECT_MAPPER.readValue(data, new TypeReference<Map<String, Object>>() {}));
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<TestWebSocketConnection> 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<Void> execute(URI url, WebSocketHandler clientHandler) {
|
||||
return execute(URI.create("/"), HttpHeaders.EMPTY, clientHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> execute(URI url, HttpHeaders headers, WebSocketHandler clientHandler) {
|
||||
TestWebSocketConnection connection = new TestWebSocketConnection(url, headers);
|
||||
this.connections.add(connection);
|
||||
return connection.connect(clientHandler, this.serverHandler);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<WebSocketMessage> clientSink = Sinks.many().unicast().onBackpressureBuffer();
|
||||
Sinks.Many<WebSocketMessage> serverSink = Sinks.many().unicast().onBackpressureBuffer();
|
||||
|
||||
Sinks.One<CloseStatus> clientStatusSink = Sinks.one();
|
||||
Sinks.One<CloseStatus> 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<WebSocketMessage> getClientMessages() {
|
||||
return this.clientSession.getSentMessages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return messages sent from the server side.
|
||||
*/
|
||||
public List<WebSocketMessage> 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<Void> connect(WebSocketHandler clientHandler, WebSocketHandler serverHandler) {
|
||||
|
||||
Mono<Void> serverMono = invokeHandler(serverHandler, this.serverSession);
|
||||
Mono<Void> clientMono = invokeHandler(clientHandler, this.clientSession);
|
||||
|
||||
Mono<Void> serverStatusMono = this.serverSession.closeStatus().then();
|
||||
Mono<Void> clientStatusMono = this.clientSession.closeStatus().then();
|
||||
|
||||
return Mono.zip(serverMono, clientMono, serverStatusMono, clientStatusMono).then();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the session and complete it when handling completes.
|
||||
*/
|
||||
private Mono<Void> 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<Void> closeClientSession(CloseStatus status) {
|
||||
return this.clientSession.close(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the connection from the server side.
|
||||
*/
|
||||
public Mono<Void> closeServerSession(CloseStatus status) {
|
||||
return this.serverSession.close(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@code CloseStatus} that may have come from either side.
|
||||
*/
|
||||
public Mono<CloseStatus> 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<Object> {
|
||||
|
||||
private final Sinks.Many<WebSocketMessage> sendSink;
|
||||
|
||||
private final Flux<WebSocketMessage> receiveFlux;
|
||||
|
||||
private final Sinks.One<CloseStatus> closeStatusSink;
|
||||
|
||||
private final Queue<WebSocketMessage> sentMessages = new ConcurrentLinkedQueue<>();
|
||||
|
||||
|
||||
TestWebSocketSession(String sessionId, URI url, HttpHeaders headers,
|
||||
Sinks.Many<WebSocketMessage> sendSink, Flux<WebSocketMessage> receiveFlux,
|
||||
Sinks.One<CloseStatus> closeStatusSink, Mono<CloseStatus> 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<WebSocketMessage> getSentMessages() {
|
||||
return new ArrayList<>(this.sentMessages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> send(Publisher<WebSocketMessage> 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<WebSocketMessage> receive() {
|
||||
return this.receiveFlux;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return !Boolean.TRUE.equals(this.closeStatusSink.scan(Scannable.Attr.TERMINATED));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<CloseStatus> closeStatus() {
|
||||
return this.closeStatusSink.asMono();
|
||||
}
|
||||
|
||||
public Mono<Void> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link HttpGraphQlTester} via {@link WebTestClient} to {@link GraphQlHttpHandler}
|
||||
* <li>{@link WebSocketGraphQlTester} via a {@link TestWebSocketConnection} to {@link GraphQlWebSocketHandler}
|
||||
* <li>{@link WebGraphQlTester} via direct call to {@link WebGraphQlHandler}
|
||||
* </ul>
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class WebGraphQlTesterBuilderTests {
|
||||
|
||||
private static final String DOCUMENT = "{ Query }";
|
||||
|
||||
|
||||
public static Stream<TesterBuilderSetup> 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<ServerResponse> 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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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:
|
||||
* <ul>
|
||||
* <li>Connect to {@link MockWebServer} and return a preset HTTP response.
|
||||
* <li>Use mock {@link WebGraphQlHandler} to return a preset {@link ExecutionResult}.
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* 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<GraphQlTesterSetup> 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<GraphQLError> errors) throws Exception;
|
||||
|
||||
void verifyRequest(Consumer<WebInput> 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<GraphQLError> errors) throws Exception {
|
||||
StringBuilder sb = new StringBuilder("{");
|
||||
if (StringUtils.hasText(data)) {
|
||||
sb.append("\"data\":").append(data);
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(errors)) {
|
||||
List<Map<String, Object>> 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<WebInput> 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<String, Object> map = new ObjectMapper().readValue(content, new TypeReference<Map<String, Object>>() {});
|
||||
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<WebInput> 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<GraphQLError> errors) throws Exception {
|
||||
ExecutionResultImpl.Builder builder = new ExecutionResultImpl.Builder();
|
||||
if (data != null) {
|
||||
builder.data(OBJECT_MAPPER.readValue(data, new TypeReference<Map<String, Object>>() {}));
|
||||
}
|
||||
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<WebInput> consumer) {
|
||||
WebInput webInput = this.bodyCaptor.getValue();
|
||||
consumer.accept(webInput);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
<Loggers>
|
||||
<Logger name="org.springframework.web" level="debug" />
|
||||
<Logger name="org.springframework.http" level="debug" />
|
||||
<Logger name="org.springframework.graphql" level="debug" />
|
||||
<Root level="error">
|
||||
<AppenderRef ref="Console" />
|
||||
</Root>
|
||||
|
||||
Reference in New Issue
Block a user