WebGraphQlTester supports HTTP header input

Closes gh-64
This commit is contained in:
Rossen Stoyanchev
2021-06-25 20:59:18 +01:00
parent 410948bde3
commit 4a6c718394
6 changed files with 185 additions and 107 deletions

View File

@@ -13,6 +13,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation project(':spring-graphql-test')
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}

View File

@@ -15,7 +15,7 @@
*/
package io.spring.sample.graphql;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.stereotype.Component;
@@ -24,7 +24,7 @@ import org.springframework.stereotype.Component;
public class EmployeeService {
public List<Employee> getAllEmployees() {
return Arrays.asList(new Employee("1", "Andi"));
return Collections.singletonList(new Employee("1", "Andi"));
}
}

View File

@@ -15,45 +15,27 @@
*/
package io.spring.sample.graphql;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions;
import org.springframework.graphql.boot.test.tester.AutoConfigureGraphQlTester;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.graphql.test.tester.WebGraphQlTester;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
// @formatter:off
@SpringBootTest()
@AutoConfigureWebTestClient
@AutoConfigureGraphQlTester
class SampleApplicationTests {
@Autowired
private ReactiveWebApplicationContext context;
private static final String BASE_URL = "https://spring.example.org/graphql";
WebTestClient client;
@BeforeEach
public void setup() {
this.client = WebTestClient
.bindToApplicationContext(this.context)
.apply(SecurityMockServerConfigurers.springSecurity())
.configureClient()
.filter(ExchangeFilterFunctions.basicAuthentication())
.defaultHeaders(headers -> {
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
})
.baseUrl(BASE_URL)
.build();
}
private WebGraphQlTester graphQlTester;
@Test
void printError() {
@@ -64,18 +46,14 @@ class SampleApplicationTests {
" }" +
"}";
client.post().uri("")
.bodyValue("{ \"query\": \"" + query + "\"}")
.exchange()
.expectStatus().isOk()
.expectBody(String.class)
.consumeWith(System.out::println);
this.graphQlTester.query(query)
.execute()
.errors()
.satisfy(System.out::println);
}
@Test
void anonoymousThenUnauthorized() {
void anonymousThenUnauthorized() {
String query = "{" +
" employees{ " +
" name" +
@@ -83,12 +61,14 @@ class SampleApplicationTests {
" }" +
"}";
client.post().uri("")
.bodyValue("{ \"query\": \"" + query + "\"}")
.exchange()
.expectStatus().isOk()
.expectBody().jsonPath("errors[0].extensions.classification").isEqualTo("UNAUTHORIZED");
this.graphQlTester.query(query)
.execute()
.errors()
.satisfy(errors -> {
assertThat(errors).hasSize(1);
assertThat(errors.get(0).getExtensions().get("classification"))
.isEqualTo(ErrorType.UNAUTHORIZED.name());
});
}
@Test
@@ -100,12 +80,15 @@ class SampleApplicationTests {
" }" +
"}";
client.post().uri("")
.headers(h -> h.setBasicAuth("rob", "rob"))
.bodyValue("{ \"query\": \"" + query + "\"}")
.exchange()
.expectStatus().isOk()
.expectBody().jsonPath("errors[0].extensions.classification").isEqualTo("FORBIDDEN");
this.graphQlTester.query(query)
.headers(headers -> headers.setBasicAuth("rob", "rob"))
.execute()
.errors()
.satisfy(errors -> {
assertThat(errors).hasSize(1);
assertThat(errors.get(0).getExtensions().get("classification"))
.isEqualTo(ErrorType.FORBIDDEN.name());
});
}
@Test
@@ -116,13 +99,9 @@ class SampleApplicationTests {
" }" +
"}";
client.post().uri("")
.bodyValue("{ \"query\": \"" + query + "\"}")
.exchange()
.expectStatus().isOk()
.expectBody().jsonPath("data.employees[0].name").isEqualTo("Andi");
this.graphQlTester.query(query)
.execute()
.path("employees[0].name").entity(String.class).isEqualTo("Andi");
}
@Test
@@ -134,15 +113,14 @@ class SampleApplicationTests {
" }" +
"}";
client.post().uri("")
.bodyValue("{ \"query\": \"" + query + "\"}")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("data.employees[0].name").isEqualTo("Andi")
.jsonPath("data.employees[0].salary").doesNotExist();
this.graphQlTester.query(query)
.execute()
.errors()
.satisfy(errors -> {
assertThat(errors).hasSize(1);
assertThat(errors.get(0).getExtensions().get("classification"))
.isEqualTo(ErrorType.UNAUTHORIZED.name());
});
}
@Test
@@ -154,16 +132,11 @@ class SampleApplicationTests {
" }" +
"}";
client.post().uri("")
.headers(h -> h.setBasicAuth("admin", "admin"))
.bodyValue("{ \"query\": \"" + query + "\"}")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("data.employees[0].name").isEqualTo("Andi")
.jsonPath("data.employees[0].salary").isEqualTo("42");
this.graphQlTester.query(query)
.headers(headers -> headers.setBasicAuth("admin", "admin"))
.execute()
.path("employees[0].name").entity(String.class).isEqualTo("Andi")
.path("employees[0].salary").entity(int.class).isEqualTo(42);
}
@Test
@@ -175,14 +148,11 @@ class SampleApplicationTests {
" }" +
"}";
client.post().uri("")
.headers(h -> h.setBasicAuth("admin", "INVALID"))
.bodyValue("{ \"query\": \"" + query + "\"}")
.exchange()
.expectStatus().isUnauthorized()
.expectBody()
.isEmpty();
assertThatThrownBy(() ->
this.graphQlTester.query(query)
.headers(headers -> headers.setBasicAuth("admin", "INVALID"))
.executeAndVerify())
.hasMessage("Status expected:<200 OK> but was:<401 UNAUTHORIZED>");
}
}

View File

@@ -66,6 +66,7 @@ class DefaultGraphQlTester implements GraphQlTester {
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
}
private final RequestStrategy requestStrategy;
@@ -77,13 +78,19 @@ class DefaultGraphQlTester implements GraphQlTester {
this.requestStrategy = requestStrategy;
}
protected RequestStrategy getRequestStrategy() {
return this.requestStrategy;
}
protected static Configuration initJsonPathConfig() {
return (jackson2Present ? Jackson2Configuration.create() : Configuration.builder().build());
}
@Override
public RequestSpec query(String query) {
return new DefaultRequestSpec(query);
return new DefaultRequestSpec(this.requestStrategy, query);
}
/**
@@ -182,7 +189,9 @@ class DefaultGraphQlTester implements GraphQlTester {
/**
* {@link RequestSpec} that collects the query, operationName, and variables.
*/
private final class DefaultRequestSpec implements RequestSpec {
protected static class DefaultRequestSpec implements RequestSpec {
private final RequestStrategy requestStrategy;
private final String query;
@@ -191,8 +200,10 @@ class DefaultGraphQlTester implements GraphQlTester {
private final Map<String, Object> variables = new LinkedHashMap<>();
private DefaultRequestSpec(String query) {
protected DefaultRequestSpec(RequestStrategy requestStrategy, String query) {
Assert.notNull(requestStrategy, "RequestStrategy is required");
Assert.notNull(query, "`query` is required");
this.requestStrategy = requestStrategy;
this.query = query;
}
@@ -216,21 +227,25 @@ class DefaultGraphQlTester implements GraphQlTester {
@Override
public ResponseSpec execute() {
RequestInput input = new RequestInput(this.query, this.operationName, this.variables);
return DefaultGraphQlTester.this.requestStrategy.execute(input);
RequestInput input = createRequestInput();
return this.requestStrategy.execute(input);
}
@Override
public void executeAndVerify() {
RequestInput input = new RequestInput(this.query, this.operationName, this.variables);
ResponseSpec spec = DefaultGraphQlTester.this.requestStrategy.execute(input);
RequestInput input = createRequestInput();
ResponseSpec spec = this.requestStrategy.execute(input);
spec.path("$.errors").valueIsEmpty();
}
@Override
public SubscriptionSpec executeSubscription() {
RequestInput input = new RequestInput(this.query, this.operationName, this.variables);
return DefaultGraphQlTester.this.requestStrategy.executeSubscription(input);
RequestInput input = createRequestInput();
return this.requestStrategy.executeSubscription(input);
}
protected RequestInput createRequestInput() {
return new RequestInput(this.query, this.operationName, this.variables);
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.graphql.test.tester;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.function.Consumer;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
@@ -51,6 +52,12 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl
}
@Override
public WebRequestSpec query(String query) {
return new DefaultWebRequestSpec(getRequestStrategy(), query);
}
/**
* {@link RequestStrategy} that works as an HTTP client with requests executed through
* {@link WebTestClient} that in turn may work connect with or without a live server
@@ -69,9 +76,20 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl
@Override
public ResponseSpec execute(RequestInput requestInput) {
EntityExchangeResult<byte[]> result = this.client.post().contentType(MediaType.APPLICATION_JSON)
.bodyValue(requestInput).exchange().expectStatus().isOk().expectHeader()
.contentType(MediaType.APPLICATION_JSON).expectBody().returnResult();
Assert.isInstanceOf(WebInput.class, requestInput);
WebInput webInput = (WebInput) requestInput;
EntityExchangeResult<byte[]> result = this.client.post()
.contentType(MediaType.APPLICATION_JSON)
.headers(headers -> headers.putAll(webInput.getHeaders()))
.bodyValue(requestInput)
.exchange()
.expectStatus()
.isOk()
.expectHeader()
.contentType(MediaType.APPLICATION_JSON)
.expectBody()
.returnResult();
byte[] bytes = result.getResponseBodyContent();
Assert.notNull(bytes, "Expected GraphQL response content");
@@ -83,9 +101,19 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl
@Override
public SubscriptionSpec executeSubscription(RequestInput requestInput) {
Assert.isInstanceOf(WebInput.class, requestInput);
WebInput webInput = (WebInput) requestInput;
FluxExchangeResult<TestExecutionResult> exchangeResult = this.client.post()
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.TEXT_EVENT_STREAM).bodyValue(requestInput)
.exchange().expectStatus().isOk().expectHeader().contentType(MediaType.TEXT_EVENT_STREAM)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.TEXT_EVENT_STREAM)
.headers(headers -> headers.putAll(webInput.getHeaders()))
.bodyValue(requestInput)
.exchange()
.expectStatus()
.isOk()
.expectHeader()
.contentType(MediaType.TEXT_EVENT_STREAM)
.returnResult(TestExecutionResult.class);
return new DefaultSubscriptionSpec(exchangeResult.getResponseBody().cast(ExecutionResult.class),
@@ -100,10 +128,6 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl
*/
private static class WebGraphQlHandlerRequestStrategy extends AbstractDirectRequestStrategy {
private static final URI DEFAULT_URL = URI.create("http://localhost:8080/graphql");
private static final HttpHeaders DEFAULT_HEADERS = new HttpHeaders();
private final WebGraphQlHandler graphQlHandler;
WebGraphQlHandlerRequestStrategy(WebGraphQlHandler handler, Configuration jsonPathConfig) {
@@ -111,12 +135,44 @@ class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQl
this.graphQlHandler = handler;
}
protected ExecutionResult executeInternal(RequestInput input) {
WebInput webInput = new WebInput(DEFAULT_URL, DEFAULT_HEADERS, input.toMap(), null);
ExecutionResult result = this.graphQlHandler.handle(webInput).block(DEFAULT_TIMEOUT);
protected ExecutionResult executeInternal(RequestInput requestInput) {
Assert.isInstanceOf(WebInput.class, requestInput);
ExecutionResult result = this.graphQlHandler.handle((WebInput) requestInput).block(DEFAULT_TIMEOUT);
Assert.notNull(result, "Expected ExecutionResult");
return result;
}
}
protected static final class DefaultWebRequestSpec extends DefaultRequestSpec implements WebRequestSpec {
private static final URI DEFAULT_URL = URI.create("");
private final HttpHeaders headers = new HttpHeaders();
public DefaultWebRequestSpec(RequestStrategy requestStrategy, String query) {
super(requestStrategy, query);
}
@Override
public WebRequestSpec header(String headerName, String... headerValues) {
for (String headerValue : headerValues) {
this.headers.add(headerName, headerValue);
}
return this;
}
@Override
public WebRequestSpec headers(Consumer<HttpHeaders> headersConsumer) {
headersConsumer.accept(this.headers);
return this;
}
@Override
protected RequestInput createRequestInput() {
RequestInput requestInput = super.createRequestInput();
return new WebInput(DEFAULT_URL, headers, requestInput.toMap(), null);
}
}
}

View File

@@ -15,7 +15,10 @@
*/
package org.springframework.graphql.test.tester;
import java.util.function.Consumer;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.http.HttpHeaders;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
@@ -79,6 +82,12 @@ import org.springframework.test.web.reactive.server.WebTestClient;
*/
public interface WebGraphQlTester extends GraphQlTester {
/**
* {@inheritDoc}
* <p>The returned spec for Web request input also allows adding HTTP headers.
*/
WebRequestSpec query(String query);
/**
* Create a {@code WebGraphQlTester} that performs GraphQL requests as an
* HTTP client through the given {@link WebTestClient}. Depending on how the
@@ -101,4 +110,31 @@ public interface WebGraphQlTester extends GraphQlTester {
return new DefaultWebGraphQlTester(handler);
}
/**
* Extends {@link GraphQlTester.RequestSpec} with further input options
* applicable to Web requests.
*/
interface WebRequestSpec extends RequestSpec {
/**
* Add the given, single header value under the given name.
* @param headerName the header name
* @param headerValues the header value(s)
* @return the same instance
*/
WebRequestSpec header(String headerName, String... headerValues);
/**
* Manipulate the request's headers with the given consumer. The
* headers provided to the consumer are "live", so that the consumer can
* be used to {@linkplain HttpHeaders#set(String, String) overwrite}
* existing header values, {@linkplain HttpHeaders#remove(Object) remove}
* values, or use any of the other {@link HttpHeaders} methods.
* @param headersConsumer a function that consumes the {@code HttpHeaders}
* @return this builder
*/
WebRequestSpec headers(Consumer<HttpHeaders> headersConsumer);
}
}