From b0aaf30a83cf218cd01b7f0bc0635c507ea3c581 Mon Sep 17 00:00:00 2001 From: Han Chen Date: Fri, 6 Sep 2019 09:48:04 -0500 Subject: [PATCH] Update Graphql integration test (#954) * Graphql IT test * Update * Remove jsonignore * Split test * make build pass * fix graphql it * Sync test with old groovy test --- .../testhelpers/graphql/GraphQLDSL.java | 26 +- .../graphql/VariableFieldSerializer.java | 63 ++ .../graphql/elements/Document.java | 15 +- .../testhelpers/graphql/elements/Field.java | 8 +- .../graphql/elements/SelectionSet.java | 2 +- .../java/com/yahoo/elide/tests/GraphQLIT.java | 616 ++++++++++++++++++ .../src/test/java/example/Book.java | 3 - 7 files changed, 712 insertions(+), 21 deletions(-) create mode 100644 elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/VariableFieldSerializer.java create mode 100644 elide-integration-tests/src/test/java/com/yahoo/elide/tests/GraphQLIT.java diff --git a/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/GraphQLDSL.java b/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/GraphQLDSL.java index 1ea323eaeb..c6d02a1986 100644 --- a/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/GraphQLDSL.java +++ b/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/GraphQLDSL.java @@ -23,7 +23,6 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.collect.ImmutableList; import java.util.Arrays; import java.util.Collections; @@ -339,7 +338,7 @@ public static VariableDefinition variableDefinition(String variable, String type * @return a field */ public static Selection field(String name, String value) { - return new Field(name, Arguments.emptyArgument(), Field.quoteValue(value)); + return new Field(null, name, Arguments.emptyArgument(), Field.quoteValue(value)); } /** @@ -351,7 +350,7 @@ public static Selection field(String name, String value) { * @return a field */ public static Selection field(String name, Number value) { - return new Field(name, Arguments.emptyArgument(), value); + return new Field(null, name, Arguments.emptyArgument(), value); } /** @@ -363,7 +362,7 @@ public static Selection field(String name, Number value) { * @return a field */ public static Selection field(String name, Boolean value) { - return new Field(name, Arguments.emptyArgument(), value); + return new Field(null, name, Arguments.emptyArgument(), value); } /** @@ -376,7 +375,7 @@ public static Selection field(String name, Boolean value) { * @return a field */ public static Selection field(String name, String value, boolean quoted) { - return new Field(name, Arguments.emptyArgument(), quoted ? Field.quoteValue(value) : value); + return new Field(null, name, Arguments.emptyArgument(), quoted ? Field.quoteValue(value) : value); } /** @@ -391,8 +390,17 @@ public static Selection field(String name, String value, boolean quoted) { * @see Object Types and Fields */ public static Selection field(String name, SelectionSet... selectionSet) { - List ss = ImmutableList.copyOf(selectionSet); - return new Field(name, Arguments.emptyArgument(), relayWrap(ss)); + List ss = Arrays.stream(selectionSet) + .map(i -> (SelectionSet) i) + .collect(Collectors.toList()); + return new Field(null, name, Arguments.emptyArgument(), relayWrap(ss)); + } + + public static Selection field(String alias, String name, SelectionSet... selectionSet) { + List ss = Arrays.stream(selectionSet) + .map(i -> (SelectionSet) i) + .collect(Collectors.toList()); + return new Field(alias, name, Arguments.emptyArgument(), relayWrap(ss)); } /** @@ -409,7 +417,7 @@ public static Selection field(String name, SelectionSet... selectionSet) { * @see Object Types and Fields */ public static Selection field(String name, Arguments arguments, SelectionSet... selectionSet) { - return new Field(name, arguments, relayWrap(Arrays.asList(selectionSet))); + return new Field(null, name, arguments, relayWrap(Arrays.asList(selectionSet))); } /** @@ -426,7 +434,7 @@ public static Selection field(String name) { } public static Selection field(String name, Arguments arguments) { - return new Field(name, arguments, null); + return new Field(null, name, arguments, null); } /** diff --git a/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/VariableFieldSerializer.java b/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/VariableFieldSerializer.java new file mode 100644 index 0000000000..947d4350a1 --- /dev/null +++ b/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/VariableFieldSerializer.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.testhelpers.graphql; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; + +/** + * A Jackson serializer for String entity field. + *

+ * {@link VariableFieldSerializer} serializes a String field differently when value can both represents a concrete value + * or a GraphQL variable. On concrete value, it outputs the original value unmodified and quoted; on variable value, it + * does not quote the original value. + *

+ * For example, given the following entity: + *

+ * {@code
+ * public class Book {
+ *
+ *     {@literal @}JsonSerialize(using = VariableFieldSerializer.class, as = String.class)
+ *     private String title;
+ * }
+ * }
+ * 
+ * A {@code Book(title="Java Concurrency in Practice")} serializes to + *
+ * {"title": "Java Concurrency in Practice"}
+ * 
+ * However a {@code Book(title="$titlePassedByClient")} serializes to + *
+ * {"title": $titlePassedByClient}
+ * 
+ * Note in the 1st serialization {@code title} value is quoted while the 2nd serialization it is not. + *

+ * To serialize a String entity field in such a way, add the following annotation to the field, as shown above: + *

+ * {@code
+ * {@literal @}JsonSerialize(using = VariableFieldSerializer.class, as = String.class)
+ * }
+ * 
+ * + * @see Variables + */ +public class VariableFieldSerializer extends JsonSerializer { + + private static final String VARIABLE_SIGN = "$"; + + @Override + public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value.startsWith(VARIABLE_SIGN)) { + // this is a variable + gen.writeRawValue(value); + } else { + gen.writeString(value); + } + } +} diff --git a/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/Document.java b/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/Document.java index 34de0e6720..6c6fd24498 100644 --- a/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/Document.java +++ b/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/Document.java @@ -56,11 +56,14 @@ public String toQuery() { * @return a string representation of a GraphQL response */ public String toResponse() { - return String.format( - "{\"data\":%s}", - getDefinitions().stream() - .map(Definition::toResponse) - .collect(Collectors.joining(" ")) - ); + String response = getDefinitions().stream() + .map(definition -> String.format("{\"data\":%s}", definition.toResponse())) + .collect(Collectors.joining(", ")); + + if (getDefinitions().size() != 1) { + return String.format("[%s]", response); + } + + return response; } } diff --git a/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/Field.java b/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/Field.java index faf94809cb..6e5ad6252e 100644 --- a/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/Field.java +++ b/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/Field.java @@ -40,13 +40,16 @@ public class Field extends Selection { private static final long serialVersionUID = -5906705888838083150L; public static Field scalarField(String name) { - return new Field(name, Arguments.emptyArgument(), null); + return new Field(null, name, Arguments.emptyArgument(), null); } public static String quoteValue(String value) { return String.format("\"%s\"", value); } + @Getter(AccessLevel.PRIVATE) + private final String alias; + /** * The "name" TOKEN defined in GraphQL grammar. */ @@ -70,7 +73,8 @@ public static String quoteValue(String value) { @Override public String toGraphQLSpec() { return String.format( - "%s%s%s", + "%s%s%s%s", + getAlias() == null ? "" : getAlias() + ": ", getName(), argument(), selection() diff --git a/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/SelectionSet.java b/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/SelectionSet.java index bdd38a1820..085986ae7b 100644 --- a/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/SelectionSet.java +++ b/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/SelectionSet.java @@ -41,7 +41,7 @@ String toResponse() { return String.format( "{%s}", getSelections().stream().map(Selection::toResponse) - .collect(Collectors.joining(" ")) + .collect(Collectors.joining(", ")) ); } } diff --git a/elide-integration-tests/src/test/java/com/yahoo/elide/tests/GraphQLIT.java b/elide-integration-tests/src/test/java/com/yahoo/elide/tests/GraphQLIT.java new file mode 100644 index 0000000000..ecadaeb460 --- /dev/null +++ b/elide-integration-tests/src/test/java/com/yahoo/elide/tests/GraphQLIT.java @@ -0,0 +1,616 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.tests; + +import static com.yahoo.elide.contrib.testhelpers.graphql.GraphQLDSL.UNQUOTED_VALUE; +import static com.yahoo.elide.contrib.testhelpers.graphql.GraphQLDSL.argument; +import static com.yahoo.elide.contrib.testhelpers.graphql.GraphQLDSL.arguments; +import static com.yahoo.elide.contrib.testhelpers.graphql.GraphQLDSL.document; +import static com.yahoo.elide.contrib.testhelpers.graphql.GraphQLDSL.field; +import static com.yahoo.elide.contrib.testhelpers.graphql.GraphQLDSL.mutation; +import static com.yahoo.elide.contrib.testhelpers.graphql.GraphQLDSL.selection; +import static com.yahoo.elide.contrib.testhelpers.graphql.GraphQLDSL.selections; +import static com.yahoo.elide.contrib.testhelpers.graphql.GraphQLDSL.variableDefinition; +import static com.yahoo.elide.contrib.testhelpers.graphql.GraphQLDSL.variableDefinitions; +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.yahoo.elide.contrib.testhelpers.graphql.VariableFieldSerializer; +import com.yahoo.elide.core.HttpStatus; +import com.yahoo.elide.initialization.IntegrationTest; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.restassured.response.ValidatableResponse; +import lombok.Getter; +import lombok.Setter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.ws.rs.core.MediaType; + +/** + * GraphQL integration tests. + */ +public class GraphQLIT extends IntegrationTest { + + private static class Book { + @Getter + @Setter + private long id; + + @Getter + @Setter + @JsonSerialize(using = VariableFieldSerializer.class, as = String.class) + private String title; + + private Collection authors = new ArrayList<>(); + } + + private static class Author { + @Getter + @Setter + private Long id; + + @Getter + @Setter + @JsonSerialize(using = VariableFieldSerializer.class, as = String.class) + private String name; + } + + private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + @BeforeEach + public void createBookAndAuthor() throws IOException { + // before each test, create a new book and a new author + Book book = new Book(); + book.setId(1); + book.setTitle("1984"); + + Author author = new Author(); + author.setId(1L); + author.setName("George Orwell"); + + String graphQLQuery = document( + mutation( + selection( + field( + "book", + arguments( + argument("op", "UPSERT"), + argument("data", book) + ), + selections( + field("id"), + field("title"), + field( + "authors", + arguments( + argument("op", "UPSERT"), + argument("data", author) + ), + selections( + field("id"), + field("name") + ) + ) + ) + ) + ) + ) + ).toQuery(); + + String expectedResponse = document( + selection( + field( + "book", + selections( + field("id", "1"), + field("title", "1984"), + field( + "authors", + selections( + field("id", "1"), + field("name", "George Orwell") + ) + ) + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLQuery, expectedResponse); + } + + @Test + public void createWithVariables() throws IOException { + // create a second book using variable + Book book = new Book(); + book.setId(2); + book.setTitle("$bookName"); + + Author author = new Author(); + author.setId(2L); + author.setName("$authorName"); + + String graphQLRequest = document( + mutation( + "myMutation", + variableDefinitions( + variableDefinition("bookName", "String"), + variableDefinition("authorName", "String") + ), + selection( + field( + "book", + arguments( + argument("op", "UPSERT"), + argument("data", book, UNQUOTED_VALUE) + ), + selections( + field("id"), + field("title"), + field( + "authors", + arguments( + argument("op", "UPSERT"), + argument("data", author, UNQUOTED_VALUE) + ), + selections( + field("id"), + field("name") + ) + ) + ) + ) + ) + ) + ).toQuery(); + + String expected = document( + selection( + field( + "book", + selections( + field("id", "2"), + field("title", "Grapes of Wrath"), + field( + "authors", + selections( + field("id", "2"), + field("name", "John Setinbeck") + ) + ) + ) + ) + ) + ).toResponse(); + + Map variables = new HashMap<>(); + variables.put("bookName", "Grapes of Wrath"); + variables.put("authorName", "John Setinbeck"); + + runQueryWithExpectedResult(graphQLRequest, variables, expected); + } + + @Test + public void fetchCollection() throws IOException { + // create a second book + createWithVariables(); + + String graphQLRequest = document( + selection( + field( + "book", + selections( + field("id"), + field("title"), + field( + "authors", + selections( + field("id"), + field("name") + ) + ) + ) + ) + ) + ).toQuery(); + + String expected = document( + selections( + field( + "book", + selections( + field("id", "1"), + field("title", "1984"), + field( + "authors", + selections( + field("id", "1"), + field("name", "George Orwell") + ) + ) + ), + selections( + field("id", "2"), + field("title", "Grapes of Wrath"), + field( + "authors", + selections( + field("id", "2"), + field("name", "John Setinbeck") + ) + ) + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expected); + } + + @Test + public void fetchRootSingle() throws IOException { + String graphQLRequest = document( + selection( + field( + "book", + argument( + argument( + "ids", + Arrays.asList("1") + ) + ), + selections( + field("id"), + field("title") + ) + ) + ) + ).toQuery(); + + String expectedResponse = document( + selection( + field( + "book", + selections( + field("id", "1"), + field("title", "1984") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expectedResponse); + } + + @Test + public void runUpdateAndFetchDifferentTransactionsBatch() throws IOException { + Book book = new Book(); + book.setId(2); + book.setTitle("my book created in batch!"); + + String graphQLRequest1 = document( + mutation( + selection( + field( + "book", + arguments( + argument("op", "UPSERT"), + argument("data", book) + ), + selections( + field("id"), + field("title") + ) + ) + ) + ) + ).toQuery(); + + String graphQLRequest2 = document( + selection( + field( + "book", + argument(argument("ids", "\"2\"")), + selections( + field("id"), + field("title") + ) + ) + ) + ).toQuery(); + + String expectedResponse = document( + selection( + field( + "book", + selections( + field("id", "2"), + field("title", "my book created in batch!") + ) + ) + ), + selections( + field( + "book", + selections( + field("id", "2"), + field("title", "my book created in batch!") + ) + ) + ) + ).toResponse(); + + compareJsonObject( + runQuery(toJsonArray(toJsonNode(graphQLRequest1), toJsonNode(graphQLRequest2))), + expectedResponse + ); + } + + @Test + public void runMultipleRequestsSameTransaction() throws IOException { + // This test demonstrates that multiple roots can be manipulated within a _single_ transaction + + String graphQLRequest = document( + selections( + field( + "book", + argument( + argument( + "ids", + Arrays.asList("1") + ) + ), + selections( + field("id"), + field("title"), + field( + "authors", + selections( + field("id"), + field("name") + ) + ) + ) + ), + field( + "author", + selections( + field("id"), + field("name") + ) + ) + ) + ).toQuery(); + + String expectedResponse = document( + selections( + field( + "book", + selections( + field("id", "1"), + field("title", "1984"), + field( + "authors", + selections( + field("id", "1"), + field("name", "George Orwell") + ) + ) + ) + ), + field( + "author", + selections( + field("id", "1"), + field("name", "George Orwell") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expectedResponse); + } + + @Test + public void runMultipleRequestsSameTransactionMutation() throws IOException { + // This test demonstrates that multiple roots can be manipulated within a _single_ transaction + // and results are consistent across a mutation. + Author author = new Author(); + author.setId(2L); + author.setName("Stephen King"); + + String graphQLRequest = document( + mutation( + selections( + field( + "book", + argument( + argument( + "ids", + Collections.singletonList("1") + ) + ), + selections( + field("id"), + field("title"), + field( + "authors", + arguments( + argument("op", "UPSERT"), + argument("data", author) + ), + selections( + field("id"), + field("name") + ) + ) + ) + ), + field( + "author", + selections( + field("id"), + field("name") + ) + ) + ) + ) + ).toQuery(); + + String expectedResponse = document( + selections( + field( + "book", + selections( + field("id", "1"), + field("title", "1984"), + field( + "authors", + selections( + field("id", "2"), + field("name", "Stephen King") + ) + ) + ) + ), + field( + "author", + selections( + field("id", "1"), + field("name", "George Orwell") + ), + selections( + field("id", "2"), + field("name", "Stephen King") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expectedResponse); + } + + @Test + public void runMultipleRequestsSameTransactionWithAliases() throws IOException { + // This test demonstrates that multiple roots can be manipulated within a _single_ transaction + String graphQLRequest = document( + selections( + field( + "firstAuthorCollection", + "author", + selections( + field("id"), + field("name") + ) + ), + field( + "secondAuthorCollection", + "author", + selections( + field("id"), + field("name") + ) + ) + ) + ).toQuery(); + + String expectedResponse = document( + selections( + field( + "firstAuthorCollection", + selections( + field("id", "1"), + field("name", "George Orwell") + ) + ), + field( + "secondAuthorCollection", + selections( + field("id", "1"), + field("name", "George Orwell") + ) + ) + ) + ).toResponse(); + + runQueryWithExpectedResult(graphQLRequest, expectedResponse); + } + + private void create(String query, Map variables) throws IOException { + runQuery(toJsonQuery(query, variables)); + } + + private void runQueryWithExpectedResult( + String graphQLQuery, + Map variables, + String expected + ) throws IOException { + compareJsonObject(runQuery(graphQLQuery, variables), expected); + } + + private void runQueryWithExpectedResult(String graphQLQuery, String expected) throws IOException { + runQueryWithExpectedResult(graphQLQuery, null, expected); + } + + private void compareJsonObject(ValidatableResponse response, String expected) throws IOException { + JsonNode responseNode = JSON_MAPPER.readTree(response.extract().body().asString()); + JsonNode expectedNode = JSON_MAPPER.readTree(expected); + assertEquals(expectedNode, responseNode); + } + + private ValidatableResponse runQuery(String query, Map variables) throws IOException { + return runQuery(toJsonQuery(query, variables)); + } + + private ValidatableResponse runQuery(String query) { + return given() + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .body(query) + .post("/graphQL") + .then() + .statusCode(HttpStatus.SC_OK); + } + + private String toJsonArray(JsonNode... nodes) throws IOException { + ArrayNode arrayNode = JsonNodeFactory.instance.arrayNode(); + for (JsonNode node : nodes) { + arrayNode.add(node); + } + return JSON_MAPPER.writeValueAsString(arrayNode); + } + + private String toJsonQuery(String query, Map variables) throws IOException { + return JSON_MAPPER.writeValueAsString(toJsonNode(query, variables)); + } + + private JsonNode toJsonNode(String query) { + return toJsonNode(query, null); + } + + private JsonNode toJsonNode(String query, Map variables) { + ObjectNode graphqlNode = JsonNodeFactory.instance.objectNode(); + graphqlNode.put("query", query); + if (variables != null) { + graphqlNode.set("variables", JSON_MAPPER.valueToTree(variables)); + } + return graphqlNode; + } +} diff --git a/elide-integration-tests/src/test/java/example/Book.java b/elide-integration-tests/src/test/java/example/Book.java index 4e3ec03650..ec91e52538 100644 --- a/elide-integration-tests/src/test/java/example/Book.java +++ b/elide-integration-tests/src/test/java/example/Book.java @@ -13,8 +13,6 @@ import com.yahoo.elide.annotation.ReadPermission; import com.yahoo.elide.annotation.SharePermission; -import com.fasterxml.jackson.annotation.JsonIgnore; - import org.hibernate.annotations.Formula; import java.util.ArrayList; @@ -47,7 +45,6 @@ public class Book extends BaseId { private String title; private String genre; private String language; - @JsonIgnore private long publishDate = 0; private Collection authors = new ArrayList<>(); private Collection chapters = new ArrayList<>();