Support query files for input in GraphQlTester

Closes gh-67
This commit is contained in:
Rossen Stoyanchev
2021-10-01 17:26:31 +01:00
parent 5a07883f6a
commit 762b9f06c2
23 changed files with 209 additions and 172 deletions

View File

@@ -0,0 +1,5 @@
{
"name": "Spring WebFlux Security Sample Schema",
"schemaPath": "src/main/resources/graphql/schema.graphqls",
"extensions": {}
}

View File

@@ -0,0 +1,5 @@
query {
employees {
name
}
}

View File

@@ -0,0 +1,6 @@
query {
employees {
name,
salary
}
}

View File

@@ -37,14 +37,7 @@ class SampleApplicationTests {
@Test
void printError() {
String query = "{" +
" employees{ " +
" name" +
" salary" +
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("employeesNamesAndSalaries")
.execute()
.errors()
.satisfy(System.out::println);
@@ -52,14 +45,7 @@ class SampleApplicationTests {
@Test
void anonymousThenUnauthorized() {
String query = "{" +
" employees{ " +
" name" +
" salary" +
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("employeesNamesAndSalaries")
.execute()
.errors()
.satisfy(errors -> {
@@ -70,14 +56,7 @@ class SampleApplicationTests {
@Test
void userRoleThenForbidden() {
String query = "{" +
" employees{ " +
" name" +
" salary" +
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("employeesNamesAndSalaries")
.headers(headers -> headers.setBasicAuth("rob", "rob"))
.execute()
.errors()
@@ -89,27 +68,14 @@ class SampleApplicationTests {
@Test
void canQueryName() {
String query = "{" +
" employees{ " +
" name" +
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("employeesNames")
.execute()
.path("employees[0].name").entity(String.class).isEqualTo("Andi");
}
@Test
void canNotQuerySalary() {
String query = "{" +
" employees{ " +
" name" +
" salary" +
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("employeesNamesAndSalaries")
.execute()
.errors()
.satisfy(errors -> {
@@ -120,14 +86,7 @@ class SampleApplicationTests {
@Test
void canQuerySalaryAsAdmin() {
String query = "{" +
" employees{ " +
" name" +
" salary" +
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("employeesNamesAndSalaries")
.headers(headers -> headers.setBasicAuth("admin", "admin"))
.execute()
.path("employees[0].name").entity(String.class).isEqualTo("Andi")
@@ -136,15 +95,8 @@ class SampleApplicationTests {
@Test
void invalidCredentials() {
String query = "{" +
" employees{ " +
" name" +
" salary" +
" }" +
"}";
assertThatThrownBy(() ->
this.graphQlTester.query(query)
this.graphQlTester.queryName("employeesNamesAndSalaries")
.headers(headers -> headers.setBasicAuth("admin", "INVALID"))
.executeAndVerify())
.hasMessage("Status expected:<200 OK> but was:<401 UNAUTHORIZED>");

View File

@@ -1,15 +1,5 @@
{
"name": "Spring WebFlux+WebSocket GraphQL Sample Schema",
"name": "Spring WebFlux WebSocket Sample Schema",
"schemaPath": "src/main/resources/graphql/schema.graphqls",
"extensions": {
"endpoints": {
"Local GraphQL Endpoint": {
"url": "http://localhost:8080/graphql",
"headers": {
"user-agent": "JS GraphQL"
},
"introspect": false
}
}
}
"extensions": {}
}

View File

@@ -0,0 +1,5 @@
{
"name": "Spring MVC HTTP Security Sample Schema",
"schemaPath": "src/main/resources/graphql/schema.graphqls",
"extensions": {}
}

View File

@@ -0,0 +1,5 @@
query {
employees {
name
}
}

View File

@@ -0,0 +1,6 @@
query {
employees {
name,
salary
}
}

View File

@@ -22,14 +22,7 @@ class SampleApplicationTests {
@Test
void printError() {
String query = "{" +
" employees{ " +
" name" +
" salary" +
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("employeesNamesAndSalaries")
.execute()
.errors()
.satisfy(System.out::println);
@@ -37,14 +30,7 @@ class SampleApplicationTests {
@Test
void anonymousThenUnauthorized() {
String query = "{" +
" employees{ " +
" name" +
" salary" +
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("employeesNamesAndSalaries")
.execute()
.errors()
.satisfy(errors -> {
@@ -55,14 +41,7 @@ class SampleApplicationTests {
@Test
void userRoleThenForbidden() {
String query = "{" +
" employees{ " +
" name" +
" salary" +
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("employeesNamesAndSalaries")
.headers(headers -> headers.setBasicAuth("rob", "rob"))
.execute()
.errors()
@@ -74,27 +53,14 @@ class SampleApplicationTests {
@Test
void canQueryName() {
String query = "{" +
" employees{ " +
" name" +
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("employeesNames")
.execute()
.path("employees[0].name").entity(String.class).isEqualTo("Andi");
}
@Test
void canNotQuerySalary() {
String query = "{" +
" employees{ " +
" name" +
" salary" +
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("employeesNamesAndSalaries")
.execute()
.errors()
.satisfy(errors -> {
@@ -105,14 +71,7 @@ class SampleApplicationTests {
@Test
void canQuerySalaryAsAdmin() {
String query = "{" +
" employees{ " +
" name" +
" salary" +
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("employeesNamesAndSalaries")
.headers(headers -> headers.setBasicAuth("admin", "admin"))
.execute()
.path("employees[0].name").entity(String.class).isEqualTo("Andi")
@@ -121,15 +80,8 @@ class SampleApplicationTests {
@Test
void invalidCredentials() {
String query = "{" +
" employees{ " +
" name" +
" salary" +
" }" +
"}";
assertThatThrownBy(() ->
this.graphQlTester.query(query)
this.graphQlTester.queryName("employeesNamesAndSalaries")
.headers(headers -> headers.setBasicAuth("admin", "INVALID"))
.executeAndVerify())
.hasMessage("Status expected:<200 OK> but was:<401 UNAUTHORIZED>");

View File

@@ -1,15 +1,5 @@
{
"name": "Spring MVC GraphQL Sample Schema",
"name": "Spring MVC HTTP Sample Schema",
"schemaPath": "src/main/resources/graphql/schema.graphqls",
"extensions": {
"endpoints": {
"Local GraphQL Endpoint": {
"url": "http://localhost:8080/graphql",
"headers": {
"user-agent": "JS GraphQL"
},
"introspect": false
}
}
}
"extensions": {}
}

View File

@@ -0,0 +1,5 @@
query {
artifactRepositories {
id
}
}

View File

@@ -0,0 +1,5 @@
query artifactRepository($id: ID!) {
artifactRepository(id: $id) {
name
}
}

View File

@@ -0,0 +1,8 @@
query project($slug: ID!) {
project(slug: $slug) {
repositoryUrl
releases {
version
}
}
}

View File

@@ -0,0 +1,5 @@
query project($slug: ID!) {
project(slug: $slug) {
repositoryUrl
}
}

View File

@@ -39,15 +39,8 @@ public class MockMvcGraphQlTests {
@Test
void jsonPath() {
String query = "{" +
" project(slug:\"spring-framework\") {" +
" releases {" +
" version" +
" }"+
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("projectReleases")
.variable("slug", "spring-framework")
.execute()
.path("project.releases[*].version")
.entityList(String.class)
@@ -57,13 +50,8 @@ public class MockMvcGraphQlTests {
@Test
void jsonContent() {
String query = "{" +
" project(slug:\"spring-framework\") {" +
" repositoryUrl" +
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("projectRepositoryUrl")
.variable("slug", "spring-framework")
.execute()
.path("project")
.matchesJson("{\"repositoryUrl\":\"http://github.com/spring-projects/spring-framework\"}");
@@ -71,15 +59,8 @@ public class MockMvcGraphQlTests {
@Test
void decodedResponse() {
String query = "{" +
" project(slug:\"spring-framework\") {" +
" releases {" +
" version" +
" }" +
" }" +
"}";
this.graphQlTester.query(query)
this.graphQlTester.queryName("projectReleases")
.variable("slug", "spring-framework")
.execute()
.path("project")
.entity(Project.class)
@@ -88,9 +69,8 @@ public class MockMvcGraphQlTests {
@Test
void querydslRepositorySingle() {
String query = "query { artifactRepository(id: \"spring-releases\") { name } }";
this.graphQlTester.query(query)
this.graphQlTester.queryName("artifactRepository")
.variable("id", "spring-releases")
.execute()
.path("artifactRepository.name")
.entity(String.class).isEqualTo("Spring Releases");
@@ -98,9 +78,7 @@ public class MockMvcGraphQlTests {
@Test
void querydslRepositoryMany() {
String query = "query { artifactRepositories { id } }";
this.graphQlTester.query(query)
this.graphQlTester.queryName("artifactRepositories")
.execute()
.path("artifactRepositories[*].id")
.entityList(String.class).containsExactly("spring-releases", "spring-milestones", "spring-snapshots");

View File

@@ -101,9 +101,9 @@ GraphQL response.
----
String query = "{" +
" project(slug:\"spring-framework\") {" +
" releases {" +
" version" +
" }"+
" releases {" +
" version" +
" }"+
" }" +
"}";
@@ -116,6 +116,40 @@ GraphQL response.
The JsonPath is relative to the "data" section of the response.
You can also create query files with extensions `.graphql` or `.gql` under `"graphql/"` on
the classpath and refer to them by file name. For example, given a file called
`projectReleases.graphql` in `src/main/resources/graphql`, with content:
[source,graphql,indent=0,subs="verbatim,quotes"]
----
query projectReleases($slug: ID!) {
project(slug: $slug) {
releases {
version
}
}
}
----
You can write the same test as follows:
[source,java,indent=0,subs="verbatim,quotes"]
----
graphQlTester.queryName("projectReleases") <1>
.variable("slug", "spring-framework") <2>
.execute()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
----
<1> Refer to the query in the file named "projectReleases".
<2> Set the `slug` variable.
[TIP]
====
The "JS GraphQL" plugin for IntelliJ supports GraphQL query files with code completion.
====
[[testing-errors]]

View File

@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -48,10 +49,14 @@ class DefaultGraphQlTester implements GraphQlTester {
private final RequestStrategy requestStrategy;
private final Function<String, String> queryNameResolver;
DefaultGraphQlTester(RequestStrategy requestStrategy) {
DefaultGraphQlTester(RequestStrategy requestStrategy, Function<String, String> queryNameResolver) {
Assert.notNull(requestStrategy, "RequestStrategy is required.");
Assert.notNull(queryNameResolver, "'queryNameResolver' is required.");
this.requestStrategy = requestStrategy;
this.queryNameResolver = queryNameResolver;
}
@@ -60,6 +65,11 @@ class DefaultGraphQlTester implements GraphQlTester {
return new DefaultRequestSpec(this.requestStrategy, query);
}
@Override
public RequestSpec<?> queryName(String queryName) {
return query(this.queryNameResolver.apply(queryName));
}
/**
* Factory for {@link GraphQlTester.ResponseSpec}, for use from

View File

@@ -65,7 +65,7 @@ class DefaultGraphQlTesterBuilder
RequestStrategy strategy = new GraphQlServiceRequestStrategy(
this.service, getErrorFilter(), initJsonPathConfig(), initResponseTimeout());
return new DefaultGraphQlTester(strategy);
return new DefaultGraphQlTester(strategy, getQueryNameResolver());
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.graphql.test.tester;
import java.net.URI;
import java.util.function.Consumer;
import java.util.function.Function;
import reactor.core.publisher.Flux;
@@ -39,11 +40,17 @@ class DefaultWebGraphQlTester implements WebGraphQlTester {
@Nullable
private final HttpHeaders defaultHeaders;
private final Function<String, String> queryNameResolver;
DefaultWebGraphQlTester(
WebRequestStrategy requestStrategy, @Nullable HttpHeaders defaultHeaders,
Function<String, String> queryNameResolver) {
DefaultWebGraphQlTester(WebRequestStrategy requestStrategy, @Nullable HttpHeaders defaultHeaders) {
Assert.notNull(requestStrategy, "WebRequestStrategy is required.");
this.requestStrategy = requestStrategy;
this.defaultHeaders = defaultHeaders;
this.queryNameResolver = queryNameResolver;
}
@@ -52,6 +59,11 @@ class DefaultWebGraphQlTester implements WebGraphQlTester {
return new DefaultWebRequestSpec(this.requestStrategy, this.defaultHeaders, query);
}
@Override
public WebRequestSpec queryName(String queryName) {
return query(this.queryNameResolver.apply(queryName));
}
/**
* Factory for {@link WebGraphQlTester.ResponseSpec}, for use from

View File

@@ -96,7 +96,7 @@ final class DefaultWebGraphQlTesterBuilder
@Override
public WebGraphQlTester build() {
return new DefaultWebGraphQlTester(initRequestStrategy(), this.headers);
return new DefaultWebGraphQlTester(initRequestStrategy(), this.headers, getQueryNameResolver());
}
private WebRequestStrategy initRequestStrategy() {

View File

@@ -52,6 +52,16 @@ public interface GraphQlTester {
*/
RequestSpec<?> query(String query);
/**
* Refer to a query by name where the given name is to look for a file with
* the same name and extension {@code ".graphql"} or {@code ".gql"} under
* classpath location {@code "graphql/"}.
* @return spec for response assertions
* @throws IllegalArgumentException if the queryName cannot be resolved
* @throws AssertionError if the response status is not 200 (OK)
*/
RequestSpec<?> queryName(String queryName);
/**
* Create a {@code GraphQlTester} that performs GraphQL requests through the

View File

@@ -15,7 +15,12 @@
*/
package org.springframework.graphql.test.tester;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.function.Function;
import java.util.function.Predicate;
import com.jayway.jsonpath.Configuration;
@@ -23,9 +28,12 @@ import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import graphql.GraphQLError;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.FileCopyUtils;
/**
* Base class support for implementations of
@@ -56,6 +64,8 @@ class GraphQlTesterBuilderSupport {
@Nullable
private Duration responseTimeout;
private final Function<String, String> queryNameResolver = new QueryNameResolver();
protected void addErrorFilter(Predicate<GraphQLError> predicate) {
this.errorFilter = (this.errorFilter != null ? errorFilter.and(predicate) : predicate);
@@ -80,6 +90,10 @@ class GraphQlTesterBuilderSupport {
return this.responseTimeout;
}
protected Function<String, String> getQueryNameResolver() {
return this.queryNameResolver;
}
protected Configuration initJsonPathConfig() {
if (this.jsonPathConfig != null) {
return this.jsonPathConfig;
@@ -97,6 +111,39 @@ class GraphQlTesterBuilderSupport {
}
private static class QueryNameResolver implements Function<String, String> {
private static final ClassPathResource LOCATION = new ClassPathResource("graphql/");
private static final String[] EXTENSIONS = new String[] {".graphql", ".gql"};
@Override
public String apply(String queryName) {
Resource queryResource = getQueryResource(queryName);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
FileCopyUtils.copy(queryResource.getInputStream(), outputStream);
}
catch (IOException ex) {
throw new IllegalArgumentException("Failed to read query from: " + LOCATION.getPath());
}
return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
}
private Resource getQueryResource(String queryName) {
for (String extension : EXTENSIONS) {
Resource resource = LOCATION.createRelative(queryName + extension);
if (resource.exists()) {
return resource;
}
}
throw new IllegalArgumentException(
"Could not find file '" + queryName + "' with extensions " + Arrays.toString(EXTENSIONS) +
" under " + LOCATION.getDescription());
}
}
private static class Jackson2Configuration {
static Configuration create() {

View File

@@ -88,6 +88,13 @@ public interface WebGraphQlTester extends GraphQlTester {
*/
WebRequestSpec query(String query);
/**
* {@inheritDoc}
* <p>The returned spec for Web request input also allows adding HTTP headers.
*/
WebRequestSpec queryName(String queryName);
/**
* Create a {@code WebGraphQlTester} that performs GraphQL requests as an