Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run integration tests through the GraphQL API #206

Merged
merged 1 commit into from
Apr 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/pr-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions core/src/test/kotlin/org/neo4j/graphql/CypherTests.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
}
}
Expand Down
117 changes: 83 additions & 34 deletions core/src/test/kotlin/org/neo4j/graphql/utils/CypherTestSuite.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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))
}
}

Expand All @@ -56,17 +60,27 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest
return tests
}

private fun createTransformationTask(title: String, globalBlocks: Map<String, ParsedBlock>, codeBlocks: Map<String, ParsedBlock>): () -> Cypher {
private fun createSchema(
globalBlocks: Map<String, ParsedBlock>,
codeBlocks: Map<String, ParsedBlock>,
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<String, ParsedBlock>,
codeBlocks: Map<String, ParsedBlock>
): () -> 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")
Expand Down Expand Up @@ -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<Cypher>): 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<String, ParsedBlock>,
codeBlocks: Map<String, ParsedBlock>,
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<String, QueryContext?> { 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<Any>()

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)
}
}

Expand Down
8 changes: 4 additions & 4 deletions core/src/test/resources/filter-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
----

Expand Down
2 changes: 1 addition & 1 deletion core/src/test/resources/issues/gh-112.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ CREATE
.GraphQL-Query
[source,graphql]
----
query {
query user( $uuid: ID ){
user(uuid: $uuid) {
uuid
name
Expand Down
14 changes: 11 additions & 3 deletions core/src/test/resources/issues/gh-147.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ query {
person(name: "Kevin Bacon") {
born
... on Actor {
__typename
namedColleagues(name: "Meg") {
__typename
... name
}
}
Expand All @@ -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
} ]
}
----
Expand All @@ -90,7 +94,9 @@ fragment name on Actor { name }
{
"personName" : "Kevin Bacon",
"personNamedColleaguesName" : "Meg",
"personScoreValue" : 7
"personNamedColleaguesValidTypes" : [ "Actor" ],
"personScoreValue" : 7,
"personValidTypes" : [ "Actor" ]
}
----

Expand All @@ -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', {
Expand Down