Skip to content

Commit a92b8ca

Browse files
authored
Run integration tests through the GraphQL API (#206)
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.
1 parent 268a40d commit a92b8ca

File tree

6 files changed

+103
-45
lines changed

6 files changed

+103
-45
lines changed

.github/workflows/pr-build.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ jobs:
2020
java-version: 11
2121
distribution: adopt
2222
- name: Run Maven build
23-
# TODO after fixing all integration tests
24-
# run: ./mvnw --no-transfer-progress -Dneo4j-graphql-java.integration-tests=true clean compile test
25-
run: ./mvnw --no-transfer-progress clean compile test
23+
run: ./mvnw --no-transfer-progress -Dneo4j-graphql-java.integration-tests=true -Dneo4j-graphql-java.generate-test-file-diff=false clean compile test
2624
- name: Publish Unit Test Results
2725
uses: EnricoMi/publish-unit-test-result-action@v1
2826
if: always()

core/src/test/kotlin/org/neo4j/graphql/CypherTests.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.neo4j.graphql
22

3+
import apoc.coll.Coll
34
import apoc.cypher.CypherFunctions
45
import org.junit.jupiter.api.*
56
import org.neo4j.graphql.utils.CypherTestSuite
@@ -22,6 +23,8 @@ class CypherTests {
2223
.newInProcessBuilder(Path.of("target/test-db"))
2324
.withProcedure(apoc.cypher.Cypher::class.java)
2425
.withFunction(CypherFunctions::class.java)
26+
.withProcedure(Coll::class.java)
27+
.withFunction(Coll::class.java)
2528
.build()
2629
}
2730
}

core/src/test/kotlin/org/neo4j/graphql/utils/CypherTestSuite.kt

Lines changed: 83 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package org.neo4j.graphql.utils
22

3+
import graphql.ExecutionInput
4+
import graphql.GraphQL
5+
import graphql.schema.DataFetcher
6+
import graphql.schema.DataFetchingEnvironment
7+
import graphql.schema.GraphQLSchema
38
import org.assertj.core.api.Assertions
49
import org.junit.jupiter.api.Assumptions
510
import org.junit.jupiter.api.DynamicNode
@@ -44,9 +49,8 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest
4449
if (neo4j != null) {
4550
val testData = globalBlocks[TEST_DATA_MARKER]
4651
val response = getOrCreateBlock(codeBlocks, GRAPHQL_RESPONSE_MARKER, "GraphQL-Response")
47-
4852
if (testData != null && response != null) {
49-
tests.add(integrationTest(testData, response, result))
53+
tests.add(integrationTest(title, globalBlocks, codeBlocks, testData, response))
5054
}
5155
}
5256

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

59-
private fun createTransformationTask(title: String, globalBlocks: Map<String, ParsedBlock>, codeBlocks: Map<String, ParsedBlock>): () -> Cypher {
63+
private fun createSchema(
64+
globalBlocks: Map<String, ParsedBlock>,
65+
codeBlocks: Map<String, ParsedBlock>,
66+
dataFetchingInterceptor: DataFetchingInterceptor? = null
67+
): GraphQLSchema {
6068
val schemaString = globalBlocks[SCHEMA_MARKER]?.code()
6169
?: throw IllegalStateException("Schema should be defined")
70+
val schemaConfig = (codeBlocks[SCHEMA_CONFIG_MARKER] ?: globalBlocks[SCHEMA_CONFIG_MARKER])?.code()
71+
?.let { return@let MAPPER.readValue(it, SchemaConfig::class.java) }
72+
?: SchemaConfig()
73+
return SchemaBuilder.buildSchema(schemaString, schemaConfig, dataFetchingInterceptor)
74+
}
6275

76+
private fun createTransformationTask(
77+
title: String,
78+
globalBlocks: Map<String, ParsedBlock>,
79+
codeBlocks: Map<String, ParsedBlock>
80+
): () -> Cypher {
6381
val transformationTask = FutureTask {
6482

65-
val schemaConfig = (codeBlocks[SCHEMA_CONFIG_MARKER] ?: globalBlocks[SCHEMA_CONFIG_MARKER])?.code()
66-
?.let { return@let MAPPER.readValue(it, SchemaConfig::class.java) }
67-
?: SchemaConfig()
68-
val schema = SchemaBuilder.buildSchema(schemaString, schemaConfig)
69-
83+
val schema = createSchema(globalBlocks, codeBlocks)
7084

7185
val request = codeBlocks[GRAPHQL_MARKER]?.code()
7286
?: throw IllegalStateException("missing graphql for $title")
@@ -139,37 +153,72 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest
139153
}
140154
}
141155

142-
private fun integrationTest(testData: ParsedBlock, response: ParsedBlock, result: () -> Cypher): DynamicNode = DynamicTest.dynamicTest("Integration Test", response.uri) {
143-
neo4j?.defaultDatabaseService()?.let { db ->
144-
db.executeTransactionally("MATCH (n) DETACH DELETE n")
145-
if (testData.code().isNotBlank()) {
146-
testData.code()
147-
.split(";")
148-
.filter { it.isNotBlank() }
149-
.forEach { db.executeTransactionally(it) }
150-
}
151-
val (cypher, params, type, variable) = result()
152-
val values = db.executeTransactionally(cypher, params) { result ->
153-
mutableMapOf(variable to result.stream().map { it[variable] }.let {
154-
when {
155-
type?.isList() == true -> it.toList()
156-
else -> it.findFirst().orElse(null)
156+
private fun setupDataFetchingInterceptor(testData: ParsedBlock): DataFetchingInterceptor {
157+
return object : DataFetchingInterceptor {
158+
override fun fetchData(env: DataFetchingEnvironment, delegate: DataFetcher<Cypher>): Any? = neo4j
159+
?.defaultDatabaseService()?.let { db ->
160+
db.executeTransactionally("MATCH (n) DETACH DELETE n")
161+
if (testData.code().isNotBlank()) {
162+
testData.code()
163+
.split(";")
164+
.filter { it.isNotBlank() }
165+
.forEach { db.executeTransactionally(it) }
157166
}
158-
})
159-
}
167+
val (cypher, params, type, variable) = delegate.get(env)
168+
return db.executeTransactionally(cypher, params) { result ->
169+
result.stream().map { it[variable] }.let {
170+
when {
171+
type?.isList() == true -> it.toList()
172+
else -> it.findFirst().orElse(null)
173+
}
174+
}
160175

161-
if (response.code.isEmpty()) {
176+
}
177+
}
178+
}
179+
}
180+
181+
private fun integrationTest(
182+
title: String,
183+
globalBlocks: Map<String, ParsedBlock>,
184+
codeBlocks: Map<String, ParsedBlock>,
185+
testData: ParsedBlock,
186+
response: ParsedBlock
187+
): DynamicNode = DynamicTest.dynamicTest("Integration Test", response.uri) {
188+
val dataFetchingInterceptor = setupDataFetchingInterceptor(testData)
189+
val request = codeBlocks[GRAPHQL_MARKER]?.code()
190+
?: throw IllegalStateException("missing graphql for $title")
191+
192+
193+
val requestParams = codeBlocks[GRAPHQL_VARIABLES_MARKER]?.code()?.parseJsonMap() ?: emptyMap()
194+
195+
val queryContext = codeBlocks[QUERY_CONFIG_MARKER]?.code()
196+
?.let<String, QueryContext?> { config -> return@let MAPPER.readValue(config, QueryContext::class.java) }
197+
?: QueryContext()
198+
199+
200+
val schema = createSchema(globalBlocks, codeBlocks, dataFetchingInterceptor)
201+
val graphql = GraphQL.newGraphQL(schema).build()
202+
val result = graphql.execute(ExecutionInput.newExecutionInput()
203+
.query(request)
204+
.variables(requestParams)
205+
.context(queryContext)
206+
.build())
207+
Assertions.assertThat(result.errors).isEmpty()
208+
209+
val values = result?.getData<Any>()
210+
211+
if (response.code.isEmpty()) {
212+
val actualCode = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(values)
213+
response.adjustedCode = actualCode
214+
} else {
215+
val expected = fixNumbers(response.code().parseJsonMap())
216+
val actual = fixNumber(values)
217+
if (!Objects.equals(expected, actual)) {
162218
val actualCode = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(values)
163219
response.adjustedCode = actualCode
164-
} else {
165-
val expected = fixNumbers(response.code().parseJsonMap())
166-
val actual = fixNumber(values)
167-
if (!Objects.equals(expected, actual)) {
168-
val actualCode = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(values)
169-
response.adjustedCode = actualCode
170-
}
171-
Assertions.assertThat(actual).isEqualTo(expected)
172220
}
221+
Assertions.assertThat(actual).isEqualTo(expected)
173222
}
174223
}
175224

core/src/test/resources/filter-tests.adoc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3206,7 +3206,7 @@ RETURN p {
32063206
.GraphQL-Query
32073207
[source,graphql]
32083208
----
3209-
{ p: company { employees(filter: { OR: [{ name: "Jane" },{name:"Joe"}]}) { name }}}
3209+
{ p: company { employees(filter: { OR: [{ name: "Jane" },{name:"Joe"}]}, orderBy: name_desc) { name }}}
32103210
----
32113211

32123212
.GraphQL-Response
@@ -3239,10 +3239,10 @@ RETURN p {
32393239
----
32403240
MATCH (p:Company)
32413241
RETURN p {
3242-
employees: [(p)<-[:WORKS_AT]-(pEmployees:Person) WHERE (pEmployees.name = $filterPEmployeesOr1Name
3243-
OR pEmployees.name = $filterPEmployeesOr2Name) | pEmployees {
3242+
employees: apoc.coll.sortMulti([(p)<-[:WORKS_AT]-(pEmployees:Person) WHERE (pEmployees.name = $filterPEmployeesOr1Name
3243+
OR pEmployees.name = $filterPEmployeesOr2Name) | pEmployees {
32443244
.name
3245-
}]
3245+
}], ['name'])
32463246
} AS p
32473247
----
32483248

core/src/test/resources/issues/gh-112.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ CREATE
3131
.GraphQL-Query
3232
[source,graphql]
3333
----
34-
query {
34+
query user( $uuid: ID ){
3535
user(uuid: $uuid) {
3636
uuid
3737
name

core/src/test/resources/issues/gh-147.adoc

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ query {
6060
person(name: "Kevin Bacon") {
6161
born
6262
... on Actor {
63+
__typename
6364
namedColleagues(name: "Meg") {
65+
__typename
6466
... name
6567
}
6668
}
@@ -75,11 +77,13 @@ fragment name on Actor { name }
7577
----
7678
{
7779
"person" : [ {
80+
"born" : 1958,
81+
"__typename" : "Actor",
7882
"namedColleagues" : [ {
83+
"__typename" : "Actor",
7984
"name" : "Meg Ryan"
8085
} ],
81-
"score" : 7,
82-
"born" : 1958
86+
"score" : 7
8387
} ]
8488
}
8589
----
@@ -90,7 +94,9 @@ fragment name on Actor { name }
9094
{
9195
"personName" : "Kevin Bacon",
9296
"personNamedColleaguesName" : "Meg",
93-
"personScoreValue" : 7
97+
"personNamedColleaguesValidTypes" : [ "Actor" ],
98+
"personScoreValue" : 7,
99+
"personValidTypes" : [ "Actor" ]
94100
}
95101
----
96102

@@ -101,10 +107,12 @@ MATCH (person:Person)
101107
WHERE person.name = $personName
102108
RETURN person {
103109
.born,
110+
__typename: head([label IN labels(person) WHERE label IN $personValidTypes]),
104111
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', {
105112
this: person,
106113
name: $personNamedColleaguesName
107114
}) | personNamedColleagues {
115+
__typename: head([label IN labels(personNamedColleagues) WHERE label IN $personNamedColleaguesValidTypes]),
108116
.name
109117
}],
110118
score: apoc.cypher.runFirstColumnSingle('WITH $this AS this, $value AS value RETURN $value', {

0 commit comments

Comments
 (0)