Add WebGraphQlHandler tests

See gh-53
This commit is contained in:
Rossen Stoyanchev
2021-05-31 21:09:23 +01:00
parent 1a1007ba59
commit ad8242e5e1
6 changed files with 259 additions and 42 deletions

View File

@@ -84,20 +84,18 @@ class DefaultWebGraphQlHandlerBuilder implements WebGraphQlHandler.Builder {
List<WebInterceptor> interceptorsToUse =
(this.interceptors != null ? this.interceptors : Collections.emptyList());
WebGraphQlHandler handler = interceptorsToUse.stream()
.reduce(WebInterceptor::andThen)
.map(interceptor -> (WebGraphQlHandler) input -> interceptor.intercept(input, createHandler()))
.orElse(createHandler());
return (CollectionUtils.isEmpty(this.accessors) ? handler :
new ThreadLocalExtractingHandler(handler, ThreadLocalAccessor.composite(this.accessors)));
}
private WebGraphQlHandler createHandler() {
return webInput -> {
ExecutionInput input = webInput.toExecutionInput();
return this.service.execute(input).map(result -> new WebOutput(webInput, result));
WebGraphQlHandler targetHandler = webInput -> {
ExecutionInput executionInput = webInput.toExecutionInput();
return this.service.execute(executionInput).map(result -> new WebOutput(webInput, result));
};
WebGraphQlHandler interceptionChain = interceptorsToUse.stream()
.reduce(WebInterceptor::andThen)
.map(interceptor -> (WebGraphQlHandler) input -> interceptor.intercept(input, targetHandler))
.orElse(targetHandler);
return (CollectionUtils.isEmpty(this.accessors) ? interceptionChain :
new ThreadLocalExtractingHandler(interceptionChain, ThreadLocalAccessor.composite(this.accessors)));
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2002-2021 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;
import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import org.springframework.graphql.execution.GraphQlSource;
/**
* {@link GraphQlSource} that wraps a pre-built {@link GraphQL} instance.
*/
public class TestGraphQlSource implements GraphQlSource {
private final GraphQL graphQl;
public TestGraphQlSource(GraphQL graphQl) {
this.graphQl = graphQl;
}
@Override
public GraphQL graphQl() {
return this.graphQl;
}
@Override
public GraphQLSchema schema() {
throw new UnsupportedOperationException();
}
}

View File

@@ -13,35 +13,45 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.execution;
package org.springframework.graphql;
import java.util.Map;
import org.springframework.graphql.execution.ThreadLocalAccessor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
/**
* {@link ThreadLocalAccessor} that operates on the ThreadLocal it is given.
*/
class TestThreadLocalAccessor implements ThreadLocalAccessor {
public class TestThreadLocalAccessor<T> implements ThreadLocalAccessor {
private final ThreadLocal<String> threadLocal;
private final ThreadLocal<T> threadLocal;
@Nullable
private Long threadId;
TestThreadLocalAccessor(ThreadLocal<String> threadLocal) {
public TestThreadLocalAccessor(ThreadLocal<T> threadLocal) {
this.threadLocal = threadLocal;
}
@Override
public void extractValues(Map<String, Object> container) {
String name = this.threadLocal.get();
saveThreadId();
T name = this.threadLocal.get();
Assert.notNull(name, "No ThreadLocal value");
container.put("name", name);
}
@Override
@SuppressWarnings("unchecked")
public void restoreValues(Map<String, Object> values) {
String name = (String) values.get("name");
checkThreadId();
T name = (T) values.get("name");
Assert.notNull(name, "No value to set");
this.threadLocal.set(name);
}
@@ -50,4 +60,18 @@ class TestThreadLocalAccessor implements ThreadLocalAccessor {
public void resetValues(Map<String, Object> values) {
this.threadLocal.remove();
}
private void saveThreadId() {
this.threadId = Thread.currentThread().getId();
}
private void checkThreadId() {
assertThat(this.threadId)
.as("No threadId to check. Was extractValues not called?")
.isNotNull();
assertThat(Thread.currentThread().getId() != this.threadId)
.as("ThreadLocal value extracted and restored on the same thread. Propagation not tested effectively.")
.isTrue();
}
}

View File

@@ -30,6 +30,7 @@ import reactor.util.context.Context;
import reactor.util.context.ContextView;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.TestThreadLocalAccessor;
import static org.assertj.core.api.Assertions.assertThat;
@@ -101,18 +102,15 @@ public class ContextDataFetcherDecoratorTests {
@Test
void dataFetcherWithThreadLocalContext() {
long threadId = Thread.currentThread().getId();
ThreadLocal<String> nameThreadLocal = new ThreadLocal<>();
nameThreadLocal.set("007");
TestThreadLocalAccessor<String> accessor = new TestThreadLocalAccessor<>(nameThreadLocal);
try {
GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }",
"Query", "greeting", env -> {
assertThat(Thread.currentThread().getId() != threadId).as("Not on async thread").isTrue();
return "Hello " + nameThreadLocal.get();
});
"Query", "greeting", env -> "Hello " + nameThreadLocal.get());
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
ContextView view = ContextManager.extractThreadLocalValues(new TestThreadLocalAccessor(nameThreadLocal));
ContextView view = ContextManager.extractThreadLocalValues(accessor);
ContextManager.setReactorContext(view, input);
ExecutionResult result = Mono.delay(Duration.ofMillis(10))

View File

@@ -31,6 +31,7 @@ import reactor.util.context.Context;
import reactor.util.context.ContextView;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.TestThreadLocalAccessor;
import static org.assertj.core.api.Assertions.assertThat;
@@ -59,9 +60,8 @@ public class ExceptionResolversExceptionHandlerTests {
List<GraphQLError> errors = result.getErrors();
assertThat(errors).hasSize(1);
GraphQLError error = errors.get(0);
assertThat(error.getMessage()).isEqualTo("Resolved error: Invalid greeting");
assertThat(error.getErrorType().toString()).isEqualTo("BAD_REQUEST");
assertThat(errors.get(0).getMessage()).isEqualTo("Resolved error: Invalid greeting");
assertThat(errors.get(0).getErrorType().toString()).isEqualTo("BAD_REQUEST");
}
@Test
@@ -81,39 +81,36 @@ public class ExceptionResolversExceptionHandlerTests {
ExecutionResult result = graphQl.executeAsync(input).get();
GraphQLError error = result.getErrors().get(0);
assertThat(error.getMessage()).isEqualTo("Resolved error: Invalid greeting, name=007");
List<GraphQLError> errors = result.getErrors();
assertThat(errors.get(0).getMessage()).isEqualTo("Resolved error: Invalid greeting, name=007");
}
@Test
void resolveExceptionWithThreadLocal() {
long threadId = Thread.currentThread().getId();
ThreadLocal<String> nameThreadLocal = new ThreadLocal<>();
nameThreadLocal.set("007");
TestThreadLocalAccessor<String> accessor = new TestThreadLocalAccessor<>(nameThreadLocal);
try {
GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }",
"Query", "greeting", env -> {
throw new IllegalArgumentException("Invalid greeting");
},
(SyncDataFetcherExceptionResolver) (ex, env) -> {
assertThat(Thread.currentThread().getId() != threadId).as("Not on async thread").isTrue();
return Collections.singletonList(
GraphqlErrorBuilder.newError(env)
.message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get())
.errorType(ErrorType.BAD_REQUEST)
.build());
});
(SyncDataFetcherExceptionResolver) (ex, env) -> Collections.singletonList(
GraphqlErrorBuilder.newError(env)
.message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get())
.errorType(ErrorType.BAD_REQUEST)
.build()));
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
ContextView view = ContextManager.extractThreadLocalValues(new TestThreadLocalAccessor(nameThreadLocal));
ContextView view = ContextManager.extractThreadLocalValues(accessor);
ContextManager.setReactorContext(view, input);
ExecutionResult result = Mono.delay(Duration.ofMillis(10))
.flatMap(aLong -> Mono.fromFuture(graphQl.executeAsync(input)))
.block();
GraphQLError error = result.getErrors().get(0);
assertThat(error.getMessage()).isEqualTo("Resolved error: Invalid greeting, name=007");
List<GraphQLError> errors = result.getErrors();
assertThat(errors.get(0).getMessage()).isEqualTo("Resolved error: Invalid greeting, name=007");
}
finally {
nameThreadLocal.remove();

View File

@@ -0,0 +1,155 @@
/*
* Copyright 2002-2021 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.web;
import java.net.URI;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import graphql.GraphQL;
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.GraphQlTestUtils;
import org.springframework.graphql.TestGraphQlSource;
import org.springframework.graphql.TestThreadLocalAccessor;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.graphql.execution.ExecutionGraphQlService;
import org.springframework.graphql.execution.SyncDataFetcherExceptionResolver;
import org.springframework.http.HttpHeaders;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WebGraphQlHandler}, common to both HTTP and WebSocket.
*/
public class WebGraphQlHandlerTests {
private static final WebInput webInput = new WebInput(
URI.create("http://abc.org"), new HttpHeaders(), Collections.singletonMap("query", "{ greeting }"), "1");
@Test
void reactorContextPropagation() {
GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }",
"Query", "greeting", env ->
Mono.deferContextual(context -> {
Object name = context.get("name");
return Mono.delay(Duration.ofMillis(50)).map(aLong -> "Hello " + name);
}));
GraphQlService service = new ExecutionGraphQlService(new TestGraphQlSource(graphQl));
WebGraphQlHandler handler = WebGraphQlHandler.builder(service).build();
WebOutput webOutput = handler.handle(webInput)
.contextWrite(context -> context.put("name", "007"))
.block();
Map<String, Object> data = webOutput.getData();
assertThat(data).hasSize(1).containsEntry("greeting", "Hello 007");
}
@Test
void reactorContextPropagationToExceptionResolver() {
GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }",
"Query", "greeting", env -> {
throw new IllegalArgumentException("Invalid greeting");
},
(ex, env) -> Mono.deferContextual(view -> Mono.just(Collections.singletonList(
GraphqlErrorBuilder.newError(env)
.message("Resolved error: " + ex.getMessage() + ", name=" + view.get("name"))
.errorType(ErrorType.BAD_REQUEST)
.build()))));
GraphQlService service = new ExecutionGraphQlService(new TestGraphQlSource(graphQl));
WebGraphQlHandler handler = WebGraphQlHandler.builder(service).build();
WebOutput webOutput = handler.handle(webInput)
.contextWrite(context -> context.put("name", "007"))
.block();
Map<String, Object> data = webOutput.getData();
assertThat(data).hasSize(1).containsEntry("greeting", null);
List<GraphQLError> errors = webOutput.getErrors();
assertThat(errors).hasSize(1);
assertThat(errors.get(0).getMessage()).isEqualTo("Resolved error: Invalid greeting, name=007");
}
@Test
void threadLocalContextPropagation() {
ThreadLocal<String> nameThreadLocal = new ThreadLocal<>();
nameThreadLocal.set("007");
TestThreadLocalAccessor<String> threadLocalAccessor = new TestThreadLocalAccessor<>(nameThreadLocal);
try {
GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }",
"Query", "greeting", env -> "Hello " + nameThreadLocal.get());
GraphQlService service = new ExecutionGraphQlService(new TestGraphQlSource(graphQl));
WebGraphQlHandler handler = WebGraphQlHandler.builder(service)
.interceptor((input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap(aLong -> next.handle(input)))
.threadLocalAccessor(threadLocalAccessor)
.build();
Map<String, Object> data = handler.handle(webInput).block().getData();
assertThat(data).hasSize(1).containsEntry("greeting", "Hello 007");
}
finally {
nameThreadLocal.remove();
}
}
@Test
void threadLocalContextPropagationToExceptionResolver() {
ThreadLocal<String> nameThreadLocal = new ThreadLocal<>();
nameThreadLocal.set("007");
TestThreadLocalAccessor<String> threadLocalAccessor = new TestThreadLocalAccessor<>(nameThreadLocal);
try {
GraphQL graphQl = GraphQlTestUtils.initGraphQl("type Query { greeting: String }",
"Query", "greeting", env -> {
throw new IllegalArgumentException("Invalid greeting");
},
(SyncDataFetcherExceptionResolver) (ex, env) -> Collections.singletonList(
GraphqlErrorBuilder.newError(env)
.message("Resolved error: " + ex.getMessage() + ", name=" + nameThreadLocal.get())
.errorType(ErrorType.BAD_REQUEST)
.build()));
GraphQlService service = new ExecutionGraphQlService(new TestGraphQlSource(graphQl));
WebGraphQlHandler handler = WebGraphQlHandler.builder(service)
.interceptor((input, next) -> Mono.delay(Duration.ofMillis(10)).flatMap(aLong -> next.handle(input)))
.threadLocalAccessor(threadLocalAccessor)
.build();
WebOutput webOutput = handler.handle(webInput).block();
List<GraphQLError> errors = webOutput.getErrors();
assertThat(errors.get(0).getMessage()).isEqualTo("Resolved error: Invalid greeting, name=007");
}
finally {
nameThreadLocal.remove();
}
}
}