From 4a6c718394fa7a84a8a86f329ed00ff137583a41 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 25 Jun 2021 20:59:18 +0100 Subject: [PATCH] WebGraphQlTester supports HTTP header input Closes gh-64 --- samples/webflux-security/build.gradle | 1 + .../sample/graphql/EmployeeService.java | 4 +- .../graphql/SampleApplicationTests.java | 138 +++++++----------- .../test/tester/DefaultGraphQlTester.java | 33 +++-- .../test/tester/DefaultWebGraphQlTester.java | 80 ++++++++-- .../graphql/test/tester/WebGraphQlTester.java | 36 +++++ 6 files changed, 185 insertions(+), 107 deletions(-) diff --git a/samples/webflux-security/build.gradle b/samples/webflux-security/build.gradle index 493aea7d..01e8ed47 100644 --- a/samples/webflux-security/build.gradle +++ b/samples/webflux-security/build.gradle @@ -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' } diff --git a/samples/webflux-security/src/main/java/io/spring/sample/graphql/EmployeeService.java b/samples/webflux-security/src/main/java/io/spring/sample/graphql/EmployeeService.java index 4d9d46b3..954ba96c 100644 --- a/samples/webflux-security/src/main/java/io/spring/sample/graphql/EmployeeService.java +++ b/samples/webflux-security/src/main/java/io/spring/sample/graphql/EmployeeService.java @@ -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 getAllEmployees() { - return Arrays.asList(new Employee("1", "Andi")); + return Collections.singletonList(new Employee("1", "Andi")); } } diff --git a/samples/webflux-security/src/test/java/io/spring/sample/graphql/SampleApplicationTests.java b/samples/webflux-security/src/test/java/io/spring/sample/graphql/SampleApplicationTests.java index 325e9cd0..1f926f96 100644 --- a/samples/webflux-security/src/test/java/io/spring/sample/graphql/SampleApplicationTests.java +++ b/samples/webflux-security/src/test/java/io/spring/sample/graphql/SampleApplicationTests.java @@ -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>"); } + } \ No newline at end of file diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java index e001a4de..6e833646 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java @@ -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 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); } } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java index 03a55af5..2126d001 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java @@ -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 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 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 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 headersConsumer) { + headersConsumer.accept(this.headers); + return this; + } + + @Override + protected RequestInput createRequestInput() { + RequestInput requestInput = super.createRequestInput(); + return new WebInput(DEFAULT_URL, headers, requestInput.toMap(), null); + } + + } + } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlTester.java index 277e5e8b..907abf19 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlTester.java @@ -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} + *

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 headersConsumer); + + } + }