From c3db344fd6bf62682bcbe28dd2493d2e599c8a9a Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Tue, 13 Apr 2021 19:43:35 +0200 Subject: [PATCH] Run integration tests through the GraphQL API With this change, integration tests will route calls directly through the GraphQL API and not just to the translator. Thus we can find potential errors in the return values faster. --- .github/workflows/pr-build.yaml | 4 +- .../kotlin/org/neo4j/graphql/CypherTests.kt | 3 + .../neo4j/graphql/utils/CypherTestSuite.kt | 117 +++++++++++++----- core/src/test/resources/filter-tests.adoc | 8 +- core/src/test/resources/issues/gh-112.adoc | 2 +- core/src/test/resources/issues/gh-147.adoc | 14 ++- 6 files changed, 103 insertions(+), 45 deletions(-) diff --git a/.github/workflows/pr-build.yaml b/.github/workflows/pr-build.yaml index f255ef93..44df6387 100644 --- a/.github/workflows/pr-build.yaml +++ b/.github/workflows/pr-build.yaml @@ -20,9 +20,7 @@ jobs: java-version: 11 distribution: adopt - name: Run Maven build -# TODO after fixing all integration tests -# run: ./mvnw --no-transfer-progress -Dneo4j-graphql-java.integration-tests=true clean compile test - run: ./mvnw --no-transfer-progress clean compile test + run: ./mvnw --no-transfer-progress -Dneo4j-graphql-java.integration-tests=true -Dneo4j-graphql-java.generate-test-file-diff=false clean compile test - name: Publish Unit Test Results uses: EnricoMi/publish-unit-test-result-action@v1 if: always() diff --git a/core/src/test/kotlin/org/neo4j/graphql/CypherTests.kt b/core/src/test/kotlin/org/neo4j/graphql/CypherTests.kt index a66dc7ac..ea77e6f1 100644 --- a/core/src/test/kotlin/org/neo4j/graphql/CypherTests.kt +++ b/core/src/test/kotlin/org/neo4j/graphql/CypherTests.kt @@ -1,5 +1,6 @@ package org.neo4j.graphql +import apoc.coll.Coll import apoc.cypher.CypherFunctions import org.junit.jupiter.api.* import org.neo4j.graphql.utils.CypherTestSuite @@ -22,6 +23,8 @@ class CypherTests { .newInProcessBuilder(Path.of("target/test-db")) .withProcedure(apoc.cypher.Cypher::class.java) .withFunction(CypherFunctions::class.java) + .withProcedure(Coll::class.java) + .withFunction(Coll::class.java) .build() } } diff --git a/core/src/test/kotlin/org/neo4j/graphql/utils/CypherTestSuite.kt b/core/src/test/kotlin/org/neo4j/graphql/utils/CypherTestSuite.kt index e6bc9124..ddbcc892 100644 --- a/core/src/test/kotlin/org/neo4j/graphql/utils/CypherTestSuite.kt +++ b/core/src/test/kotlin/org/neo4j/graphql/utils/CypherTestSuite.kt @@ -1,5 +1,10 @@ package org.neo4j.graphql.utils +import graphql.ExecutionInput +import graphql.GraphQL +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import graphql.schema.GraphQLSchema import org.assertj.core.api.Assertions import org.junit.jupiter.api.Assumptions import org.junit.jupiter.api.DynamicNode @@ -44,9 +49,8 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest if (neo4j != null) { val testData = globalBlocks[TEST_DATA_MARKER] val response = getOrCreateBlock(codeBlocks, GRAPHQL_RESPONSE_MARKER, "GraphQL-Response") - if (testData != null && response != null) { - tests.add(integrationTest(testData, response, result)) + tests.add(integrationTest(title, globalBlocks, codeBlocks, testData, response)) } } @@ -56,17 +60,27 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest return tests } - private fun createTransformationTask(title: String, globalBlocks: Map, codeBlocks: Map): () -> Cypher { + private fun createSchema( + globalBlocks: Map, + codeBlocks: Map, + dataFetchingInterceptor: DataFetchingInterceptor? = null + ): GraphQLSchema { val schemaString = globalBlocks[SCHEMA_MARKER]?.code() ?: throw IllegalStateException("Schema should be defined") + val schemaConfig = (codeBlocks[SCHEMA_CONFIG_MARKER] ?: globalBlocks[SCHEMA_CONFIG_MARKER])?.code() + ?.let { return@let MAPPER.readValue(it, SchemaConfig::class.java) } + ?: SchemaConfig() + return SchemaBuilder.buildSchema(schemaString, schemaConfig, dataFetchingInterceptor) + } + private fun createTransformationTask( + title: String, + globalBlocks: Map, + codeBlocks: Map + ): () -> Cypher { val transformationTask = FutureTask { - val schemaConfig = (codeBlocks[SCHEMA_CONFIG_MARKER] ?: globalBlocks[SCHEMA_CONFIG_MARKER])?.code() - ?.let { return@let MAPPER.readValue(it, SchemaConfig::class.java) } - ?: SchemaConfig() - val schema = SchemaBuilder.buildSchema(schemaString, schemaConfig) - + val schema = createSchema(globalBlocks, codeBlocks) val request = codeBlocks[GRAPHQL_MARKER]?.code() ?: throw IllegalStateException("missing graphql for $title") @@ -139,37 +153,72 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest } } - private fun integrationTest(testData: ParsedBlock, response: ParsedBlock, result: () -> Cypher): DynamicNode = DynamicTest.dynamicTest("Integration Test", response.uri) { - neo4j?.defaultDatabaseService()?.let { db -> - db.executeTransactionally("MATCH (n) DETACH DELETE n") - if (testData.code().isNotBlank()) { - testData.code() - .split(";") - .filter { it.isNotBlank() } - .forEach { db.executeTransactionally(it) } - } - val (cypher, params, type, variable) = result() - val values = db.executeTransactionally(cypher, params) { result -> - mutableMapOf(variable to result.stream().map { it[variable] }.let { - when { - type?.isList() == true -> it.toList() - else -> it.findFirst().orElse(null) + private fun setupDataFetchingInterceptor(testData: ParsedBlock): DataFetchingInterceptor { + return object : DataFetchingInterceptor { + override fun fetchData(env: DataFetchingEnvironment, delegate: DataFetcher): Any? = neo4j + ?.defaultDatabaseService()?.let { db -> + db.executeTransactionally("MATCH (n) DETACH DELETE n") + if (testData.code().isNotBlank()) { + testData.code() + .split(";") + .filter { it.isNotBlank() } + .forEach { db.executeTransactionally(it) } } - }) - } + val (cypher, params, type, variable) = delegate.get(env) + return db.executeTransactionally(cypher, params) { result -> + result.stream().map { it[variable] }.let { + when { + type?.isList() == true -> it.toList() + else -> it.findFirst().orElse(null) + } + } - if (response.code.isEmpty()) { + } + } + } + } + + private fun integrationTest( + title: String, + globalBlocks: Map, + codeBlocks: Map, + testData: ParsedBlock, + response: ParsedBlock + ): DynamicNode = DynamicTest.dynamicTest("Integration Test", response.uri) { + val dataFetchingInterceptor = setupDataFetchingInterceptor(testData) + val request = codeBlocks[GRAPHQL_MARKER]?.code() + ?: throw IllegalStateException("missing graphql for $title") + + + val requestParams = codeBlocks[GRAPHQL_VARIABLES_MARKER]?.code()?.parseJsonMap() ?: emptyMap() + + val queryContext = codeBlocks[QUERY_CONFIG_MARKER]?.code() + ?.let { config -> return@let MAPPER.readValue(config, QueryContext::class.java) } + ?: QueryContext() + + + val schema = createSchema(globalBlocks, codeBlocks, dataFetchingInterceptor) + val graphql = GraphQL.newGraphQL(schema).build() + val result = graphql.execute(ExecutionInput.newExecutionInput() + .query(request) + .variables(requestParams) + .context(queryContext) + .build()) + Assertions.assertThat(result.errors).isEmpty() + + val values = result?.getData() + + if (response.code.isEmpty()) { + val actualCode = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(values) + response.adjustedCode = actualCode + } else { + val expected = fixNumbers(response.code().parseJsonMap()) + val actual = fixNumber(values) + if (!Objects.equals(expected, actual)) { val actualCode = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(values) response.adjustedCode = actualCode - } else { - val expected = fixNumbers(response.code().parseJsonMap()) - val actual = fixNumber(values) - if (!Objects.equals(expected, actual)) { - val actualCode = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(values) - response.adjustedCode = actualCode - } - Assertions.assertThat(actual).isEqualTo(expected) } + Assertions.assertThat(actual).isEqualTo(expected) } } diff --git a/core/src/test/resources/filter-tests.adoc b/core/src/test/resources/filter-tests.adoc index 3710d72f..96e1dc3d 100644 --- a/core/src/test/resources/filter-tests.adoc +++ b/core/src/test/resources/filter-tests.adoc @@ -3206,7 +3206,7 @@ RETURN p { .GraphQL-Query [source,graphql] ---- -{ p: company { employees(filter: { OR: [{ name: "Jane" },{name:"Joe"}]}) { name }}} +{ p: company { employees(filter: { OR: [{ name: "Jane" },{name:"Joe"}]}, orderBy: name_desc) { name }}} ---- .GraphQL-Response @@ -3239,10 +3239,10 @@ RETURN p { ---- MATCH (p:Company) RETURN p { - employees: [(p)<-[:WORKS_AT]-(pEmployees:Person) WHERE (pEmployees.name = $filterPEmployeesOr1Name - OR pEmployees.name = $filterPEmployeesOr2Name) | pEmployees { + employees: apoc.coll.sortMulti([(p)<-[:WORKS_AT]-(pEmployees:Person) WHERE (pEmployees.name = $filterPEmployeesOr1Name + OR pEmployees.name = $filterPEmployeesOr2Name) | pEmployees { .name - }] + }], ['name']) } AS p ---- diff --git a/core/src/test/resources/issues/gh-112.adoc b/core/src/test/resources/issues/gh-112.adoc index 112879a4..0953179d 100644 --- a/core/src/test/resources/issues/gh-112.adoc +++ b/core/src/test/resources/issues/gh-112.adoc @@ -31,7 +31,7 @@ CREATE .GraphQL-Query [source,graphql] ---- -query { +query user( $uuid: ID ){ user(uuid: $uuid) { uuid name diff --git a/core/src/test/resources/issues/gh-147.adoc b/core/src/test/resources/issues/gh-147.adoc index e82d024a..260e7cb2 100644 --- a/core/src/test/resources/issues/gh-147.adoc +++ b/core/src/test/resources/issues/gh-147.adoc @@ -60,7 +60,9 @@ query { person(name: "Kevin Bacon") { born ... on Actor { + __typename namedColleagues(name: "Meg") { + __typename ... name } } @@ -75,11 +77,13 @@ fragment name on Actor { name } ---- { "person" : [ { + "born" : 1958, + "__typename" : "Actor", "namedColleagues" : [ { + "__typename" : "Actor", "name" : "Meg Ryan" } ], - "score" : 7, - "born" : 1958 + "score" : 7 } ] } ---- @@ -90,7 +94,9 @@ fragment name on Actor { name } { "personName" : "Kevin Bacon", "personNamedColleaguesName" : "Meg", - "personScoreValue" : 7 + "personNamedColleaguesValidTypes" : [ "Actor" ], + "personScoreValue" : 7, + "personValidTypes" : [ "Actor" ] } ---- @@ -101,10 +107,12 @@ MATCH (person:Person) WHERE person.name = $personName RETURN person { .born, + __typename: head([label IN labels(person) WHERE label IN $personValidTypes]), namedColleagues: [personNamedColleagues IN apoc.cypher.runFirstColumnMany('WITH $this AS this, $name AS name WITH $this AS this MATCH (this)-[:ACTED_IN]->()<-[:ACTED_IN]-(other) WHERE other.name CONTAINS $name RETURN other', { this: person, name: $personNamedColleaguesName }) | personNamedColleagues { + __typename: head([label IN labels(personNamedColleagues) WHERE label IN $personNamedColleaguesValidTypes]), .name }], score: apoc.cypher.runFirstColumnSingle('WITH $this AS this, $value AS value RETURN $value', {