Update and expand GraphQlTester tests

See gh-317
This commit is contained in:
rstoyanchev
2022-03-04 13:10:02 +00:00
parent 29b1aa2cc7
commit 53fb1d4dc0
9 changed files with 811 additions and 428 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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>