Add WebGraphQlHandler tests
See gh-53
This commit is contained in:
@@ -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)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user