Skip to content

Commit a4736c9

Browse files
authored
Merge pull request #902 from graphql-java-kickstart/bugfix/886
Fail with a clear error when unions contain themselves
2 parents a24067d + c70aaed commit a4736c9

2 files changed

Lines changed: 75 additions & 2 deletions

File tree

‎src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,11 @@ class SchemaParser internal constructor(
345345
return builder.build()
346346
}
347347

348-
private fun getLeafUnionObjects(definition: UnionTypeDefinition, types: List<GraphQLObjectType>): List<GraphQLObjectType> {
348+
private fun getLeafUnionObjects(
349+
definition: UnionTypeDefinition,
350+
types: List<GraphQLObjectType>,
351+
path: List<UnionTypeDefinition> = listOf(definition),
352+
): List<GraphQLObjectType> {
349353
val name = definition.name
350354
val leafObjects = mutableListOf<GraphQLObjectType>()
351355

@@ -356,7 +360,11 @@ class SchemaParser internal constructor(
356360
val nestedUnion: UnionTypeDefinition? = unionDefinitions.find { otherDefinition -> typeName == otherDefinition.name }
357361

358362
if (nestedUnion != null) {
359-
leafObjects.addAll(getLeafUnionObjects(nestedUnion, types))
363+
if (nestedUnion in path) {
364+
val cycle = (path.dropWhile { union -> union != nestedUnion } + nestedUnion).joinToString(" -> ") { union -> union.name }
365+
throw SchemaError("Union '$typeName' contains itself through $cycle${it.sourceLocation.describe()}.")
366+
}
367+
leafObjects.addAll(getLeafUnionObjects(nestedUnion, types, path + nestedUnion))
360368
} else {
361369
leafObjects.add(types.find { type -> type.name == typeName }
362370
?: throw SchemaError("Expected object type '$typeName' for union type '$name', but found none!"))

‎src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,67 @@ class SchemaParserTest {
277277
assertEquals(error.message, "Type 'Nested' used by input field 'TestInput.nested' at line 6, column 14 is not defined in the schema.")
278278
}
279279

280+
@Test
281+
fun `parser should throw descriptive exception when unions list each other`() {
282+
val error = assertThrows(SchemaError::class.java) {
283+
unionParser(
284+
"""
285+
|union Result = Book | Media
286+
|union Media = Movie | Result
287+
""".trimMargin())
288+
}
289+
290+
assertEquals(error.message, "Union 'Result' contains itself through Result -> Media -> Result at line 6, column 23.")
291+
}
292+
293+
@Test
294+
fun `parser should throw descriptive exception when nested unions list each other`() {
295+
val error = assertThrows(SchemaError::class.java) {
296+
unionParser(
297+
"""
298+
|union Result = Book | A
299+
|union A = B
300+
|union B = C
301+
|union C = A | Movie
302+
""".trimMargin())
303+
}
304+
305+
assertEquals(error.message, "Union 'A' contains itself through A -> B -> C -> A at line 8, column 11.")
306+
}
307+
308+
@Test
309+
fun `parser should throw descriptive exception when union lists itself`() {
310+
val error = assertThrows(SchemaError::class.java) {
311+
unionParser("union Result = Book | Result")
312+
}
313+
314+
assertEquals(error.message, "Union 'Result' contains itself through Result -> Result at line 5, column 23.")
315+
}
316+
317+
private fun unionParser(unions: String) = SchemaParser.newParser()
318+
.schemaString(
319+
"""
320+
|type Query {
321+
| results: [Result!]!
322+
|}
323+
|
324+
|$unions
325+
|
326+
|type Book {
327+
| title: String!
328+
|}
329+
|
330+
|type Movie {
331+
| title: String!
332+
|}
333+
""".trimMargin())
334+
.resolvers(object : GraphQLQueryResolver {
335+
fun results(): List<Any> = listOf()
336+
})
337+
.dictionary(Book::class, Movie::class)
338+
.build()
339+
.makeExecutableSchema()
340+
280341
@Test
281342
fun `parser should throw descriptive exception when type was never reached by the scanner`() {
282343
val error = assertThrows(SchemaError::class.java) {
@@ -1062,6 +1123,10 @@ class SchemaParserTest {
10621123

10631124
class RootObj
10641125

1126+
data class Book(val title: String)
1127+
1128+
data class Movie(val title: String)
1129+
10651130
class ProxiedResolver : GraphQLQueryResolver {
10661131
fun test(): List<String> = listOf()
10671132
}

0 commit comments

Comments
 (0)